From 1697615483406fbe90ff780db3199678dfe6eb48 Mon Sep 17 00:00:00 2001 From: krahets Date: Fri, 10 Apr 2026 22:40:59 +0800 Subject: [PATCH] deploy --- assets/javascripts/bundle.c2b142ea.min.js | 2 +- chapter_data_structure/character_encoding/index.html | 5 ++--- en/assets/javascripts/bundle.c2b142ea.min.js | 2 +- en/chapter_array_and_linkedlist/list/index.html | 2 +- en/chapter_data_structure/character_encoding/index.html | 5 ++--- en/javascripts/animation_player.js | 2 +- en/javascripts/katex.js | 2 +- en/javascripts/mathjax.js | 2 +- en/javascripts/starfield.js | 2 +- en/search.json | 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/chapter_data_structure/character_encoding/index.html | 5 ++--- ja/javascripts/animation_player.js | 2 +- ja/javascripts/katex.js | 2 +- ja/javascripts/mathjax.js | 2 +- ja/javascripts/starfield.js | 2 +- ja/search.json | 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_data_structure/character_encoding/index.html | 5 ++--- 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 +- search.json | 2 +- stylesheets/animation_player.css | 2 +- stylesheets/extra.css | 2 +- stylesheets/giscus-dark.css | 2 +- stylesheets/giscus-light.css | 2 +- zh-hant/assets/javascripts/bundle.c2b142ea.min.js | 2 +- zh-hant/chapter_data_structure/character_encoding/index.html | 5 ++--- 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/search.json | 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 +- 56 files changed, 61 insertions(+), 66 deletions(-) diff --git a/assets/javascripts/bundle.c2b142ea.min.js b/assets/javascripts/bundle.c2b142ea.min.js index 66264a9c9..b465a5c07 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: 20260410024918 */ +/*! update cache: 20260410223920 */ diff --git a/chapter_data_structure/character_encoding/index.html b/chapter_data_structure/character_encoding/index.html index 15a153a7a..a56e6f058 100644 --- a/chapter_data_structure/character_encoding/index.html +++ b/chapter_data_structure/character_encoding/index.html @@ -4511,9 +4511,8 @@

3.4.3   Unicode 字符集

随着计算机技术的蓬勃发展,字符集与编码标准百花齐放,而这带来了许多问题。一方面,这些字符集一般只定义了特定语言的字符,无法在多语言环境下正常工作。另一方面,同一种语言存在多种字符集标准,如果两台计算机使用的是不同的编码标准,则在信息传递时就会出现乱码。

那个时代的研究人员就在想:如果推出一个足够完整的字符集,将世界范围内的所有语言和符号都收录其中,不就可以解决跨语言环境和乱码问题了吗?在这种想法的驱动下,一个大而全的字符集 Unicode 应运而生。

-

Unicode 的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。

-

自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截至 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号甚至表情符号等。在庞大的 Unicode 字符集中,常用的字符占用 2 字节,有些生僻的字符占用 3 字节甚至 4 字节。

-

Unicode 是一种通用字符集,本质上是给每个字符分配一个编号(称为“码点”),但它并没有规定在计算机中如何存储这些字符码点。我们不禁会问:当多种长度的 Unicode 码点同时出现在一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符?

+

Unicode 的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截至 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号甚至表情符号等。

+

Unicode 作为一种通用字符集,本质上是给每个字符分配唯一的“码点”(字符编号),其取值范围为 U+0000 至 U+10FFFF,构成了统一的字符编号空间。然而,Unicode 并没有规定在计算机中如何存储这些字符码点。我们不禁会问:当多种长度的 Unicode 码点同时出现在一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符?

对于以上问题,一种直接的解决方案是将所有字符存储为等长的编码。如图 3-7 所示,“Hello”中的每个字符占用 1 字节,“算法”中的每个字符占用 2 字节。我们可以通过高位填 0 将“Hello 算法”中的所有字符都编码为 2 字节长度。这样系统就可以每隔 2 字节解析一个字符,恢复这个短语的内容了。

Unicode 编码示例

图 3-7   Unicode 编码示例

diff --git a/en/assets/javascripts/bundle.c2b142ea.min.js b/en/assets/javascripts/bundle.c2b142ea.min.js index 958384251..ec66c8aa1 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: 20260410024939 */ +/*! update cache: 20260410223942 */ diff --git a/en/chapter_array_and_linkedlist/list/index.html b/en/chapter_array_and_linkedlist/list/index.html index 83ca64005..ce6e88bdd 100644 --- a/en/chapter_array_and_linkedlist/list/index.html +++ b/en/chapter_array_and_linkedlist/list/index.html @@ -4501,7 +4501,7 @@

4.3   List

-

A list is an abstract data structure that represents an ordered collection of elements. It supports operations such as element access, modification, insertion, deletion, and traversal, without requiring users to worry about capacity limits. Lists can be implemented using linked lists or arrays.

+

A list is an abstract data structure concept that represents an ordered collection of elements, supporting operations such as element access, modification, insertion, deletion, and traversal, without requiring users to consider capacity limitations. Lists can be implemented based on linked lists or arrays.

  • A linked list can naturally be viewed as a list: it supports insertion, deletion, search, and update, and can grow flexibly as needed.
  • An array also supports insertion, deletion, search, and update, but because its length is fixed, it can only be regarded as a list with a capacity limit.
  • diff --git a/en/chapter_data_structure/character_encoding/index.html b/en/chapter_data_structure/character_encoding/index.html index 01bf1bb25..e5152697a 100644 --- a/en/chapter_data_structure/character_encoding/index.html +++ b/en/chapter_data_structure/character_encoding/index.html @@ -4437,9 +4437,8 @@

    3.4.3   Unicode Character Set

    With the vigorous development of computer technology, character sets and encoding standards flourished, which brought many problems. On the one hand, these character sets generally only define characters for specific languages and cannot work normally in multilingual environments. On the other hand, multiple character set standards exist for the same language, and if two computers use different encoding standards, garbled characters will appear during information transmission.

    Researchers of that era thought: If a sufficiently complete character set were released to include all languages and symbols in the world, wouldn't that solve problems in cross-language environments and eliminate garbled text? Driven by this idea, a large and comprehensive character set, Unicode, was born.

    -

    Unicode, or Unified Code, can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards.

    -

    Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. In practical storage and encoding schemes for this vast character set, commonly used characters often occupy 2 bytes, while some rare characters occupy 3 bytes or even 4 bytes.

    -

    Unicode is a universal character set that essentially assigns a number (called a "code point") to each character, but it does not specify how to store these character code points in computers. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters?

    +

    Unicode, or Unified Code, can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards. Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages.

    +

    As a universal character set, Unicode essentially assigns each character a unique "code point" (character identifier), whose range is U+0000 to U+10FFFF, forming a unified character numbering space. However, Unicode does not specify how to store these character code points in computers. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters?

    For the above problem, a straightforward solution is to store all characters as equal-length encodings. As shown in Figure 3-7, each character in "Hello" occupies 1 byte, and each character in "算法" (algorithm) occupies 2 bytes. We can encode all characters in "Hello 算法" as 2 bytes in length by padding the high bits with 0. In this way, the system can parse one character every 2 bytes and restore the content of this phrase.

    Unicode encoding example

    Figure 3-7   Unicode encoding example

    diff --git a/en/javascripts/animation_player.js b/en/javascripts/animation_player.js index 7cebd1409..293c9c952 100644 --- a/en/javascripts/animation_player.js +++ b/en/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410024939 */ +/*! update cache: 20260410223942 */ diff --git a/en/javascripts/katex.js b/en/javascripts/katex.js index 4ae8eaac2..4b5bb285a 100644 --- a/en/javascripts/katex.js +++ b/en/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410024939 */ +/*! update cache: 20260410223942 */ diff --git a/en/javascripts/mathjax.js b/en/javascripts/mathjax.js index aa9627b2c..8c92639df 100644 --- a/en/javascripts/mathjax.js +++ b/en/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260410024939 */ +/*! update cache: 20260410223942 */ diff --git a/en/javascripts/starfield.js b/en/javascripts/starfield.js index b6da0b576..8c5604bbd 100644 --- a/en/javascripts/starfield.js +++ b/en/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410024939 */ +/*! update cache: 20260410223942 */ diff --git a/en/search.json b/en/search.json index 3c5da0a7d..05b8b9d64 100644 --- a/en/search.json +++ b/en/search.json @@ -1 +1 @@ -{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"Chapter 16.   Appendix","text":"","path":["Chapter 16. Appendix","Chapter 16.   Appendix"],"tags":[]},{"location":"chapter_appendix/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 16.1   Programming Environment Installation
    • 16.2   Contributing Together
    • 16.3   Glossary
    ","path":["Chapter 16. Appendix","Chapter 16.   Appendix"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   Contributing Together","text":"

    Due to limited capacity, there may be inevitable omissions and errors in this book. We appreciate your understanding and are grateful for your help in correcting them. If you discover typos, broken links, missing content, ambiguous wording, unclear explanations, or structural issues, please help us make corrections to provide readers with higher-quality learning resources.

    The GitHub IDs of all contributors will be displayed on the homepage of the book repository, the web version, and the PDF version to acknowledge their selfless contributions to the open source community.

    The Charm of Open Source

    The interval between two printings of a physical book is often quite long, making content updates very inconvenient.

    In this open source book, the time for content updates has been shortened to just days or even hours.

    ","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#1-minor-content-adjustments","level":3,"title":"1.   Minor Content Adjustments","text":"

    As shown in Figure 16-3, there is an \"edit icon\" in the top-right corner of each page. You can modify text or code by following these steps.

    1. Click the \"edit icon\". If you encounter a prompt asking you to \"Fork this repository\", please approve the operation.
    2. Modify the content of the Markdown source file, verify the correctness of the content, and maintain consistent formatting as much as possible.
    3. Fill in a description of your changes at the bottom of the page, then click the \"Propose file change\" button. After the new page loads, click the \"Create pull request\" button to submit your pull request.

    Figure 16-3   Page edit button

    Images cannot be directly modified. Please describe the issue by creating a new Issue or leaving a comment. We will promptly redraw and replace the images.

    ","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#2-content-creation","level":3,"title":"2.   Content Creation","text":"

    If you are interested in contributing to this open source project, including translating code into other programming languages or expanding article content, you will need to follow the Pull Request workflow below.

    1. Log in to GitHub and Fork the book's code repository to your personal account.
    2. Go to your forked repository page and use the git clone command to clone the repository to your local machine.
    3. Create content locally and conduct comprehensive tests to verify code correctness.
    4. Commit your local changes and push them to the remote repository.
    5. Refresh the repository webpage and click the \"Create pull request\" button to submit your pull request.
    ","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker-deployment","level":3,"title":"3.   Docker Deployment","text":"

    From the root directory of hello-algo, run the following Docker command to access the project at http://localhost:8000:

    docker-compose up -d\n

    Use the following command to remove the deployment:

    docker-compose down\n
    ","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   Programming Environment Installation","text":"","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1611-installing-ide","level":2,"title":"16.1.1   Installing IDE","text":"

    We recommend using the open-source and lightweight VS Code as the local integrated development environment (IDE). Visit the VS Code official website, and download and install the appropriate version of VS Code according to your operating system.

    Figure 16-1   Download VS Code from the Official Website

    VS Code has a powerful ecosystem of extensions that supports running and debugging most programming languages. For example, after installing the \"Python Extension Pack\" extension, you can debug Python code. The installation steps are shown in the following figure.

    Figure 16-2   Install VS Code Extensions

    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1612-installing-language-environments","level":2,"title":"16.1.2   Installing Language Environments","text":"","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1-python-environment","level":3,"title":"1.   Python Environment","text":"
    1. Download and install Miniconda3 with Python 3.10 or later.
    2. Search for python in the VS Code extension marketplace and install the Python Extension Pack.
    3. (Optional) Enter pip install black on the command line to install the code formatter.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc-environment","level":3,"title":"2.   C/C++ Environment","text":"
    1. Windows systems need to install MinGW (configuration tutorial); macOS comes with Clang built-in and does not require installation.
    2. Search for c++ in the VS Code extension marketplace and install the C/C++ Extension Pack.
    3. (Optional) Open the Settings page, search for the Clang_format_fallback Style code formatting option, and set it to { BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#3-java-environment","level":3,"title":"3.   Java Environment","text":"
    1. Download and install OpenJDK (version 10 or later).
    2. Search for java in the VS Code extension marketplace and install the Extension Pack for Java.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#4-c-environment","level":3,"title":"4.   C# Environment","text":"
    1. Download and install .NET 8.0.
    2. Search for C# Dev Kit in the VS Code extension marketplace and install C# Dev Kit (configuration tutorial).
    3. You can also use Visual Studio (installation tutorial).
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#5-go-environment","level":3,"title":"5.   Go Environment","text":"
    1. Download and install Go.
    2. Search for go in the VS Code extension marketplace and install Go.
    3. Press Ctrl + Shift + P to open the command palette, type go, select Go: Install/Update Tools, check all options and install.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift-environment","level":3,"title":"6.   Swift Environment","text":"
    1. Download and install Swift.
    2. Search for swift in the VS Code extension marketplace and install Swift for Visual Studio Code.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript-environment","level":3,"title":"7.   JavaScript Environment","text":"
    1. Download and install Node.js.
    2. (Optional) Search for Prettier in the VS Code extension marketplace and install the code formatter.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript-environment","level":3,"title":"8.   TypeScript Environment","text":"
    1. Follow the same installation steps as the JavaScript environment.
    2. Install TypeScript Execute (tsx).
    3. Search for typescript in the VS Code extension marketplace and install Pretty TypeScript Errors.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart-environment","level":3,"title":"9.   Dart Environment","text":"
    1. Download and install Dart.
    2. Search for dart in the VS Code extension marketplace and install Dart.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust-environment","level":3,"title":"10.   Rust Environment","text":"
    1. Download and install Rust.
    2. Search for rust in the VS Code extension marketplace and install rust-analyzer.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   Glossary","text":"

    The following table lists important terms that appear in this book.

    Table 16-1   Important Terms in Data Structures and Algorithms

    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 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 binary search tree balanced binary search tree balance factor heap max heap min heap priority queue heapify top-\\(k\\) problem 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 dynamic programming initial state state-transition equation knapsack problem edit distance problem greedy algorithm","path":["Chapter 16. Appendix","16.3   Glossary"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"Chapter 4.   Arrays and Linked Lists","text":"

    Abstract

    The world of data structures is like a solid brick wall.

    The bricks of an array are neatly aligned, each pressed tightly against the next. The bricks of a linked list are scattered about, with connecting vines weaving freely through the gaps between them.

    ","path":["Chapter 4. Arrays and Linked Lists","Chapter 4.   Arrays and Linked Lists"],"tags":[]},{"location":"chapter_array_and_linkedlist/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 4.1   Array
    • 4.2   Linked List
    • 4.3   List
    • 4.4   Random-Access Memory and Cache *
    • 4.5   Summary
    ","path":["Chapter 4. Arrays and Linked Lists","Chapter 4.   Arrays and Linked Lists"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   Array","text":"

    An array is a linear data structure that stores elements of the same type in contiguous memory space. The position of an element in the array is called the element's index. Figure 4-1 illustrates the main concepts and storage method of arrays.

    Figure 4-1   Array definition and storage method

    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411-common-array-operations","level":2,"title":"4.1.1   Common Array Operations","text":"","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1-initializing-arrays","level":3,"title":"1.   Initializing Arrays","text":"

    We can choose between two array initialization methods based on our needs: with or without initial values. When no initial values are specified, most programming languages initialize array elements to \\(0\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    # Initialize array\narr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n
    array.cpp
    /* Initialize array */\n// Stored on stack\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// Stored on heap (requires manual memory release)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n
    array.java
    /* Initialize array */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n
    array.cs
    /* Initialize array */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n
    array.go
    /* Initialize array */\nvar arr [5]int\n// In Go, specifying length ([5]int) creates an array; not specifying length ([]int) creates a slice\n// Since Go's arrays are designed to have their length determined at compile time, only constants can be used to specify the length\n// For convenience in implementing the extend() method, slices are treated as arrays below\nnums := []int{1, 3, 2, 5, 4}\n
    array.swift
    /* Initialize array */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n
    array.js
    /* Initialize array */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n
    array.ts
    /* Initialize array */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n
    array.dart
    /* Initialize array */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n
    array.rs
    /* Initialize array */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// In Rust, specifying length ([i32; 5]) creates an array; not specifying length (&[i32]) creates a slice\n// Since Rust's arrays are designed to have their length determined at compile time, only constants can be used to specify the length\n// Vector is the type generally used as a dynamic array in Rust\n// For convenience in implementing the extend() method, vectors are treated as arrays below\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
    array.c
    /* Initialize array */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n
    array.kt
    /* Initialize array */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n
    array.rb
    # Initialize array\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2-accessing-elements","level":3,"title":"2.   Accessing Elements","text":"

    Array elements are stored in contiguous memory space, which means calculating the memory address of array elements is very easy. Given the array's memory address (the memory address of the first element) and an element's index, we can use the formula shown in Figure 4-2 to calculate the element's memory address and directly access that element.

    Figure 4-2   Memory address calculation for array elements

    Observing Figure 4-2, we find that the first element of an array has an index of \\(0\\), which may seem counterintuitive since counting from \\(1\\) would be more natural. However, from the perspective of the address calculation formula, an index is essentially an offset from the memory address. The address offset of the first element is \\(0\\), so it is reasonable for its index to be \\(0\\).

    Accessing elements in an array is highly efficient; we can randomly access any element in the array in \\(O(1)\\) time.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def random_access(nums: list[int]) -> int:\n    \"\"\"Random access to element\"\"\"\n    # Randomly select a number from the interval [0, len(nums)-1]\n    random_index = random.randint(0, len(nums) - 1)\n    # Retrieve and return the random element\n    random_num = nums[random_index]\n    return random_num\n
    array.cpp
    /* Random access to element */\nint randomAccess(int *nums, int size) {\n    // Randomly select a number from interval [0, size)\n    int randomIndex = rand() % size;\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
    array.java
    /* Random access to element */\nint randomAccess(int[] nums) {\n    // Randomly select a number in the interval [0, nums.length)\n    int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
    array.cs
    /* Random access to element */\nint RandomAccess(int[] nums) {\n    Random random = new();\n    // Randomly select a number in interval [0, nums.Length)\n    int randomIndex = random.Next(nums.Length);\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
    array.go
    /* Random access to element */\nfunc randomAccess(nums []int) (randomNum int) {\n    // Randomly select a number in the interval [0, nums.length)\n    randomIndex := rand.Intn(len(nums))\n    // Retrieve and return the random element\n    randomNum = nums[randomIndex]\n    return\n}\n
    array.swift
    /* Random access to element */\nfunc randomAccess(nums: [Int]) -> Int {\n    // Randomly select a number in interval [0, nums.count)\n    let randomIndex = nums.indices.randomElement()!\n    // Retrieve and return the random element\n    let randomNum = nums[randomIndex]\n    return randomNum\n}\n
    array.js
    /* Random access to element */\nfunction randomAccess(nums) {\n    // Randomly select a number in the interval [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Retrieve and return the random element\n    const random_num = nums[random_index];\n    return random_num;\n}\n
    array.ts
    /* Random access to element */\nfunction randomAccess(nums: number[]): number {\n    // Randomly select a number in the interval [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Retrieve and return the random element\n    const random_num = nums[random_index];\n    return random_num;\n}\n
    array.dart
    /* Random access to element */\nint randomAccess(List<int> nums) {\n  // Randomly select a number in the interval [0, nums.length)\n  int randomIndex = Random().nextInt(nums.length);\n  // Retrieve and return the random element\n  int randomNum = nums[randomIndex];\n  return randomNum;\n}\n
    array.rs
    /* Random access to element */\nfn random_access(nums: &[i32]) -> i32 {\n    // Randomly select a number in interval [0, nums.len())\n    let random_index = rand::thread_rng().gen_range(0..nums.len());\n    // Retrieve and return the random element\n    let random_num = nums[random_index];\n    random_num\n}\n
    array.c
    /* Random access to element */\nint randomAccess(int *nums, int size) {\n    // Randomly select a number from interval [0, size)\n    int randomIndex = rand() % size;\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
    array.kt
    /* Random access to element */\nfun randomAccess(nums: IntArray): Int {\n    // Randomly select a number in interval [0, nums.size)\n    val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n    // Retrieve and return the random element\n    val randomNum = nums[randomIndex]\n    return randomNum\n}\n
    array.rb
    ### Random access element ###\ndef random_access(nums)\n  # Randomly select a number in the interval [0, nums.length)\n  random_index = Random.rand(0...nums.length)\n\n  # Retrieve and return the random element\n  nums[random_index]\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3-inserting-elements","level":3,"title":"3.   Inserting Elements","text":"

    Array elements are packed tightly together in memory, with no extra space between them for additional data. As shown in Figure 4-3, if we want to insert an element in the middle of an array, we need to shift all subsequent elements one position to the right and then assign the value at that index.

    Figure 4-3   Example of inserting an element into an array

    It is worth noting that since the length of an array is fixed, inserting an element will inevitably push the last element out of the array. We will leave the solution to this problem for discussion in the \"List\" chapter.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def insert(nums: list[int], num: int, index: int):\n    \"\"\"Insert element num at index index in the array\"\"\"\n    # Move all elements at and after index index backward by one position\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # Assign num to the element at index index\n    nums[index] = num\n
    array.cpp
    /* Insert element num at index index in the array */\nvoid insert(int *nums, int size, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.java
    /* Insert element num at index index in the array */\nvoid insert(int[] nums, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.cs
    /* Insert element num at index index in the array */\nvoid Insert(int[] nums, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.go
    /* Insert element num at index index in the array */\nfunc insert(nums []int, num int, index int) {\n    // Move all elements at and after index index backward by one position\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
    array.swift
    /* Insert element num at index index in the array */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // Move all elements at and after index index backward by one position\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
    array.js
    /* Insert element num at index index in the array */\nfunction insert(nums, num, index) {\n    // Move all elements at and after index index backward by one position\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.ts
    /* Insert element num at index index in the array */\nfunction insert(nums: number[], num: number, index: number): void {\n    // Move all elements at and after index index backward by one position\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.dart
    /* Insert element _num at array index index */\nvoid insert(List<int> nums, int _num, int index) {\n  // Move all elements at and after index index backward by one position\n  for (var i = nums.length - 1; i > index; i--) {\n    nums[i] = nums[i - 1];\n  }\n  // Assign _num to element at index\n  nums[index] = _num;\n}\n
    array.rs
    /* Insert element num at index index in the array */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // Move all elements at and after index index backward by one position\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.c
    /* Insert element num at index index in the array */\nvoid insert(int *nums, int size, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.kt
    /* Insert element num at index index in the array */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // Move all elements at and after index index backward by one position\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
    array.rb
    ### Insert element num at index in array ###\ndef insert(nums, num, index)\n  # Move all elements at and after index index backward by one position\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # Assign num to the element at index index\n  nums[index] = num\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4-removing-elements","level":3,"title":"4.   Removing Elements","text":"

    Similarly, as shown in Figure 4-4, to delete the element at index \\(i\\), we need to shift all elements after index \\(i\\) forward by one position.

    Figure 4-4   Example of removing an element from an array

    Note that after the deletion is complete, the original last element is no longer meaningful, so we do not need to modify it explicitly.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def remove(nums: list[int], index: int):\n    \"\"\"Remove the element at index index\"\"\"\n    # Move all elements after index index forward by one position\n    for i in range(index, len(nums) - 1):\n        nums[i] = nums[i + 1]\n
    array.cpp
    /* Remove the element at index index */\nvoid remove(int *nums, int size, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.java
    /* Remove the element at index index */\nvoid remove(int[] nums, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.cs
    /* Remove the element at index index */\nvoid Remove(int[] nums, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < nums.Length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.go
    /* Remove the element at index index */\nfunc remove(nums []int, index int) {\n    // Move all elements after index index forward by one position\n    for i := index; i < len(nums)-1; i++ {\n        nums[i] = nums[i+1]\n    }\n}\n
    array.swift
    /* Remove the element at index index */\nfunc remove(nums: inout [Int], index: Int) {\n    // Move all elements after index index forward by one position\n    for i in nums.indices.dropFirst(index).dropLast() {\n        nums[i] = nums[i + 1]\n    }\n}\n
    array.js
    /* Remove the element at index index */\nfunction remove(nums, index) {\n    // Move all elements after index index forward by one position\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.ts
    /* Remove the element at index index */\nfunction remove(nums: number[], index: number): void {\n    // Move all elements after index index forward by one position\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.dart
    /* Remove the element at index index */\nvoid remove(List<int> nums, int index) {\n  // Move all elements after index index forward by one position\n  for (var i = index; i < nums.length - 1; i++) {\n    nums[i] = nums[i + 1];\n  }\n}\n
    array.rs
    /* Remove the element at index index */\nfn remove(nums: &mut [i32], index: usize) {\n    // Move all elements after index index forward by one position\n    for i in index..nums.len() - 1 {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.c
    /* Remove the element at index index */\n// Note: stdio.h occupies the remove keyword\nvoid removeItem(int *nums, int size, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.kt
    /* Remove the element at index index */\nfun remove(nums: IntArray, index: Int) {\n    // Move all elements after index index forward by one position\n    for (i in index..<nums.size - 1) {\n        nums[i] = nums[i + 1]\n    }\n}\n
    array.rb
    ### Delete element at index ###\ndef remove(nums, index)\n  # Move all elements after index index forward by one position\n  for i in index...(nums.length - 1)\n    nums[i] = nums[i + 1]\n  end\nend\n

    Overall, array insertion and deletion operations have the following drawbacks:

    • High time complexity: The average time complexity for both insertion and deletion in arrays is \\(O(n)\\), where \\(n\\) is the length of the array.
    • Loss of elements: Since the length of an array is immutable, after inserting an element, elements that exceed the array's length will be lost.
    • Memory waste: We can initialize a relatively long array and use only the front portion, so that any overwritten tail elements are merely unused placeholders, but this wastes some memory space.
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5-traversing-arrays","level":3,"title":"5.   Traversing Arrays","text":"

    In most programming languages, we can traverse an array either by index or by directly iterating through each element in the array:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def traverse(nums: list[int]):\n    \"\"\"Traverse array\"\"\"\n    count = 0\n    # Traverse array by index\n    for i in range(len(nums)):\n        count += nums[i]\n    # Direct traversal of array elements\n    for num in nums:\n        count += num\n    # Traverse simultaneously data index and elements\n    for i, num in enumerate(nums):\n        count += nums[i]\n        count += num\n
    array.cpp
    /* Traverse array */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
    array.java
    /* Traverse array */\nvoid traverse(int[] nums) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (int num : nums) {\n        count += num;\n    }\n}\n
    array.cs
    /* Traverse array */\nvoid Traverse(int[] nums) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < nums.Length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    foreach (int num in nums) {\n        count += num;\n    }\n}\n
    array.go
    /* Traverse array */\nfunc traverse(nums []int) {\n    count := 0\n    // Traverse array by index\n    for i := 0; i < len(nums); i++ {\n        count += nums[i]\n    }\n    count = 0\n    // Direct traversal of array elements\n    for _, num := range nums {\n        count += num\n    }\n    // Traverse simultaneously data index and elements\n    for i, num := range nums {\n        count += nums[i]\n        count += num\n    }\n}\n
    array.swift
    /* Traverse array */\nfunc traverse(nums: [Int]) {\n    var count = 0\n    // Traverse array by index\n    for i in nums.indices {\n        count += nums[i]\n    }\n    // Direct traversal of array elements\n    for num in nums {\n        count += num\n    }\n    // Traverse simultaneously data index and elements\n    for (i, num) in nums.enumerated() {\n        count += nums[i]\n        count += num\n    }\n}\n
    array.js
    /* Traverse array */\nfunction traverse(nums) {\n    let count = 0;\n    // Traverse array by index\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (const num of nums) {\n        count += num;\n    }\n}\n
    array.ts
    /* Traverse array */\nfunction traverse(nums: number[]): void {\n    let count = 0;\n    // Traverse array by index\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (const num of nums) {\n        count += num;\n    }\n}\n
    array.dart
    /* Traverse array elements */\nvoid traverse(List<int> nums) {\n  int count = 0;\n  // Traverse array by index\n  for (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n  }\n  // Direct traversal of array elements\n  for (int _num in nums) {\n    count += _num;\n  }\n  // Traverse array using forEach method\n  nums.forEach((_num) {\n    count += _num;\n  });\n}\n
    array.rs
    /* Traverse array */\nfn traverse(nums: &[i32]) {\n    let mut _count = 0;\n    // Traverse array by index\n    for i in 0..nums.len() {\n        _count += nums[i];\n    }\n    // Direct traversal of array elements\n    _count = 0;\n    for &num in nums {\n        _count += num;\n    }\n}\n
    array.c
    /* Traverse array */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
    array.kt
    /* Traverse array */\nfun traverse(nums: IntArray) {\n    var count = 0\n    // Traverse array by index\n    for (i in nums.indices) {\n        count += nums[i]\n    }\n    // Direct traversal of array elements\n    for (j in nums) {\n        count += j\n    }\n}\n
    array.rb
    ### Traverse array ###\ndef traverse(nums)\n  count = 0\n\n  # Traverse array by index\n  for i in 0...nums.length\n    count += nums[i]\n  end\n\n  # Direct traversal of array elements\n  for num in nums\n    count += num\n  end\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6-finding-elements","level":3,"title":"6.   Finding Elements","text":"

    Finding a specified element in an array requires traversing the array and checking whether the element value matches in each iteration; if it matches, output the corresponding index.

    Since an array is a linear data structure, the above search operation is called a \"linear search\".

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def find(nums: list[int], target: int) -> int:\n    \"\"\"Find the specified element in the array\"\"\"\n    for i in range(len(nums)):\n        if nums[i] == target:\n            return i\n    return -1\n
    array.cpp
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    ### Find specified element in array ###\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":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7-expanding-arrays","level":3,"title":"7.   Expanding Arrays","text":"

    In complex system environments, programs cannot guarantee that the memory space after an array is available, making it unsafe to expand the array's capacity. Therefore, in most programming languages, the length of an array is immutable.

    If we want to expand an array, we need to create a new, larger array and then copy the original array elements to the new array one by one. This is an \\(O(n)\\) operation, which is very time-consuming when the array is large. The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def extend(nums: list[int], enlarge: int) -> list[int]:\n    \"\"\"Extend array length\"\"\"\n    # Initialize an array with extended length\n    res = [0] * (len(nums) + enlarge)\n    # Copy all elements from the original array to the new array\n    for i in range(len(nums)):\n        res[i] = nums[i]\n    # Return the extended new array\n    return res\n
    array.cpp
    /* Extend array length */\nint *extend(int *nums, int size, int enlarge) {\n    // Initialize an array with extended length\n    int *res = new int[size + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Free memory\n    delete[] nums;\n    // Return the extended new array\n    return res;\n}\n
    array.java
    /* Extend array length */\nint[] extend(int[] nums, int enlarge) {\n    // Initialize an array with extended length\n    int[] res = new int[nums.length + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.cs
    /* Extend array length */\nint[] Extend(int[] nums, int enlarge) {\n    // Initialize an array with extended length\n    int[] res = new int[nums.Length + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < nums.Length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.go
    /* Extend array length */\nfunc extend(nums []int, enlarge int) []int {\n    // Initialize an array with extended length\n    res := make([]int, len(nums)+enlarge)\n    // Copy all elements from the original array to the new array\n    for i, num := range nums {\n        res[i] = num\n    }\n    // Return the extended new array\n    return res\n}\n
    array.swift
    /* Extend array length */\nfunc extend(nums: [Int], enlarge: Int) -> [Int] {\n    // Initialize an array with extended length\n    var res = Array(repeating: 0, count: nums.count + enlarge)\n    // Copy all elements from the original array to the new array\n    for i in nums.indices {\n        res[i] = nums[i]\n    }\n    // Return the extended new array\n    return res\n}\n
    array.js
    /* Extend array length */\n// Note: JavaScript's Array is dynamic array, can be directly expanded\n// For learning purposes, this function treats Array as fixed-length array\nfunction extend(nums, enlarge) {\n    // Initialize an array with extended length\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Copy all elements from the original array to the new array\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.ts
    /* Extend array length */\n// Note: TypeScript's Array is dynamic array, can be directly expanded\n// For learning purposes, this function treats Array as fixed-length array\nfunction extend(nums: number[], enlarge: number): number[] {\n    // Initialize an array with extended length\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Copy all elements from the original array to the new array\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.dart
    /* Extend array length */\nList<int> extend(List<int> nums, int enlarge) {\n  // Initialize an array with extended length\n  List<int> res = List.filled(nums.length + enlarge, 0);\n  // Copy all elements from the original array to the new array\n  for (var i = 0; i < nums.length; i++) {\n    res[i] = nums[i];\n  }\n  // Return the extended new array\n  return res;\n}\n
    array.rs
    /* Extend array length */\nfn extend(nums: &[i32], enlarge: usize) -> Vec<i32> {\n    // Initialize an array with extended length\n    let mut res: Vec<i32> = vec![0; nums.len() + enlarge];\n    // Copy all elements from original array to new\n    res[0..nums.len()].copy_from_slice(nums);\n\n    // Return the extended new array\n    res\n}\n
    array.c
    /* Extend array length */\nint *extend(int *nums, int size, int enlarge) {\n    // Initialize an array with extended length\n    int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Initialize expanded space\n    for (int i = size; i < size + enlarge; i++) {\n        res[i] = 0;\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.kt
    /* Extend array length */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n    // Initialize an array with extended length\n    val res = IntArray(nums.size + enlarge)\n    // Copy all elements from the original array to the new array\n    for (i in nums.indices) {\n        res[i] = nums[i]\n    }\n    // Return the extended new array\n    return res\n}\n
    array.rb
    ### Extend array length ###\n# Note: Ruby's Array is dynamic array, can be directly expanded\n# For learning purposes, this function treats Array as fixed-length array\ndef extend(nums, enlarge)\n  # Initialize an array with extended length\n  res = Array.new(nums.length + enlarge, 0)\n\n  # Copy all elements from the original array to the new array\n  for i in 0...nums.length\n    res[i] = nums[i]\n  end\n\n  # Return the extended new array\n  res\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412-advantages-and-limitations-of-arrays","level":2,"title":"4.1.2   Advantages and Limitations of Arrays","text":"

    Arrays are stored in contiguous memory space with elements of the same type. This approach contains rich prior information that the system can use to optimize the efficiency of data structure operations.

    • High space efficiency: Arrays allocate contiguous memory blocks for data without additional structural overhead.
    • Support for random access: Arrays allow accessing any element in \\(O(1)\\) time.
    • Cache locality: When accessing array elements, the computer not only loads the element but also caches the surrounding data, thereby leveraging the cache to improve the execution speed of subsequent operations.

    Contiguous space storage is a double-edged sword with the following limitations:

    • Low insertion and deletion efficiency: When an array has many elements, insertion and deletion operations require shifting a large number of elements.
    • Immutable length: After an array is initialized, its length is fixed. Expanding the array requires copying all data to a new array, which is very costly.
    • Space waste: If the allocated size of an array exceeds what is actually needed, the extra space is wasted.
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413-typical-applications-of-arrays","level":2,"title":"4.1.3   Typical Applications of Arrays","text":"

    Arrays are a fundamental and common data structure, frequently used in various algorithms and for implementing various complex data structures.

    • Random access: If we want to randomly sample some items, we can use an array to store them and generate a random sequence to implement random sampling based on indices.
    • Sorting and searching: Arrays are the most commonly used data structure for sorting and searching algorithms. Quick sort, merge sort, binary search, and others are primarily performed on arrays.
    • Lookup tables: When we need to quickly find an element or its corresponding relationship, we can use an array as a lookup table. For example, if we want to implement a mapping from characters to ASCII codes, we can use the ASCII code value of a character as an index, with the corresponding element stored at that position in the array.
    • Machine learning: Neural networks make extensive use of linear algebra operations between vectors, matrices, and tensors, all of which are constructed in the form of arrays. Arrays are the most commonly used data structure in neural network programming.
    • Data structure implementation: Arrays can be used to implement stacks, queues, hash tables, heaps, graphs, and other data structures. For example, the adjacency matrix representation of a graph is essentially a two-dimensional array.
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   Linked List","text":"

    Memory is a shared resource for all programs. In a complex runtime environment, free memory may be scattered throughout the address space. We know that arrays require contiguous memory, and when an array is very large, the system may not be able to provide such a large contiguous block. This is where the flexibility of linked lists becomes apparent.

    A linked list is a linear data structure in which each element is a node object, and the nodes are connected through \"references\". A reference records the memory address of the next node, through which the next node can be accessed from the current node.

    This design allows linked-list nodes to be stored in different locations in memory, and their addresses do not need to be contiguous.

    Figure 4-5   Linked list definition and storage method

    Observing Figure 4-5, the basic unit of a linked list is a node object. Each node contains two pieces of data: the node's \"value\" and a \"reference\" to the next node.

    • The first node of a linked list is called the \"head node\", and the last node is called the \"tail node\".
    • The tail node points to \"null\", which is denoted as null, nullptr, and None in Java, C++, and Python, respectively.
    • In languages that support pointers, such as C, C++, Go, and Rust, the aforementioned \"reference\" should be replaced with \"pointer\".

    As shown in the following code, a linked list node ListNode contains not only a value but also an additional reference (pointer). Therefore, linked lists occupy more memory space than arrays when storing the same amount of data.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class ListNode:\n    \"\"\"Linked list node class\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val               # Node value\n        self.next: ListNode | None = None # Reference to the next node\n
    /* Linked list node structure */\nstruct ListNode {\n    int val;         // Node value\n    ListNode *next;  // Pointer to the next node\n    ListNode(int x) : val(x), next(nullptr) {}  // Constructor\n};\n
    /* Linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode next;  // Reference to the next node\n    ListNode(int x) { val = x; }  // Constructor\n}\n
    /* Linked list node class */\nclass ListNode(int x) {  // Constructor\n    int val = x;         // Node value\n    ListNode? next;      // Reference to the next node\n}\n
    /* Linked list node structure */\ntype ListNode struct {\n    Val  int       // Node value\n    Next *ListNode // Pointer to the next node\n}\n\n// NewListNode Constructor, creates a new linked list\nfunc NewListNode(val int) *ListNode {\n    return &ListNode{\n        Val:  val,\n        Next: nil,\n    }\n}\n
    /* Linked list node class */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Reference to the next node\n\n    init(x: Int) { // Constructor\n        val = x\n    }\n}\n
    /* Linked list node class */\nclass ListNode {\n    constructor(val, next) {\n        this.val = (val === undefined ? 0 : val);       // Node value\n        this.next = (next === undefined ? null : next); // Reference to the next node\n    }\n}\n
    /* Linked list node class */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    constructor(val?: number, next?: ListNode | null) {\n        this.val = val === undefined ? 0 : val;        // Node value\n        this.next = next === undefined ? null : next;  // Reference to the next node\n    }\n}\n
    /* Linked list node class */\nclass ListNode {\n  int val; // Node value\n  ListNode? next; // Reference to the next node\n  ListNode(this.val, [this.next]); // Constructor\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n/* Linked list node class */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Node value\n    next: Option<Rc<RefCell<ListNode>>>, // Pointer to the next node\n}\n
    /* Linked list node structure */\ntypedef struct ListNode {\n    int val;               // Node value\n    struct ListNode *next; // Pointer to the next node\n} ListNode;\n\n/* Constructor */\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
    /* Linked list node class */\n// Constructor\nclass ListNode(x: Int) {\n    val _val: Int = x          // Node value\n    val next: ListNode? = null // Reference to the next node\n}\n
    # Linked list node class\nclass ListNode\n  attr_accessor :val  # Node value\n  attr_accessor :next # Reference to the next node\n\n  def initialize(val=0, next_node=nil)\n    @val = val\n    @next = next_node\n  end\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421-common-linked-list-operations","level":2,"title":"4.2.1   Common Linked List Operations","text":"","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1-initializing-a-linked-list","level":3,"title":"1.   Initializing a Linked List","text":"

    Building a linked list involves two steps: first, initializing each node object; second, constructing the reference relationships between nodes. Once initialization is complete, we can traverse all nodes starting from the head node of the linked list through the reference next.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    # Initialize linked list 1 -> 3 -> 2 -> 5 -> 4\n# Initialize each node\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
    linked_list.cpp
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
    linked_list.java
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.cs
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.go
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// Build references between nodes\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n
    linked_list.swift
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
    linked_list.js
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.ts
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.dart
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\\\n// Initialize each node\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.rs
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\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
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// Build references between nodes\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
    linked_list.kt
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.rb
    # Initialize linked list 1 -> 3 -> 2 -> 5 -> 4\n# Initialize each node\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
    Code Visualization

    Full Screen >

    An array is a single variable; for example, an array nums contains elements nums[0], nums[1], and so on. A linked list, by contrast, is composed of multiple independent node objects. We usually use the head node as a stand-in for the entire linked list; for example, the linked list in the above code can be referred to as linked list n0.

    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2-inserting-a-node","level":3,"title":"2.   Inserting a Node","text":"

    Inserting a node in a linked list is very easy. As shown in Figure 4-6, suppose we want to insert a new node P between two adjacent nodes n0 and n1. We only need to change two node references (pointers), with a time complexity of \\(O(1)\\).

    In contrast, the time complexity of inserting an element in an array is \\(O(n)\\), which is inefficient when dealing with large amounts of data.

    Figure 4-6   Example of inserting a node into a linked list

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def insert(n0: ListNode, P: ListNode):\n    \"\"\"Insert node P after node n0 in the linked list\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
    linked_list.cpp
    /* Insert node P after node n0 in the linked list */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.java
    /* Insert node P after node n0 in the linked list */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.cs
    /* Insert node P after node n0 in the linked list */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.go
    /* Insert node P after node n0 in the linked list */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
    linked_list.swift
    /* Insert node P after node n0 in the linked list */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
    linked_list.js
    /* Insert node P after node n0 in the linked list */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.ts
    /* Insert node P after node n0 in the linked list */\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
    /* Insert node P after node n0 in the linked list */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
    linked_list.rs
    /* Insert node P after node n0 in the linked list */\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
    /* Insert node P after node n0 in the linked list */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.kt
    /* Insert node P after node n0 in the linked list */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
    linked_list.rb
    ### Insert node _p after node n0 in linked list ###\n# Ruby's `p` is a built-in function, `P` is a constant, so use `_p` instead\ndef insert(n0, _p)\n  n1 = n0.next\n  _p.next = n1\n  n0.next = _p\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3-removing-a-node","level":3,"title":"3.   Removing a Node","text":"

    As shown in Figure 4-7, removing a node in a linked list is also very convenient. We only need to change one node's reference (pointer).

    Note that although node P still points to n1 after the deletion operation is complete, the linked list can no longer access P when traversing, which means P no longer belongs to this linked list.

    Figure 4-7   Removing a node from a linked list

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def remove(n0: ListNode):\n    \"\"\"Remove the first node after node n0 in the linked list\"\"\"\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
    /* Remove the first node after node n0 in the linked list */\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    // Free memory\n    delete P;\n}\n
    linked_list.java
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\n// Note: stdio.h occupies the remove keyword\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    // Free memory\n    free(P);\n}\n
    linked_list.kt
    /* Remove the first node after node n0 in the linked list */\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
    ### Delete first node after node n0 in linked list ###\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":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4-accessing-a-node","level":3,"title":"4.   Accessing a Node","text":"

    Accessing nodes in a linked list is less efficient. As mentioned in the previous section, we can access any element in an array in \\(O(1)\\) time. This is not the case with linked lists. The program needs to start from the head node and traverse backward one by one until the target node is found. That is, accessing the \\(i\\)-th node in a linked list requires \\(i - 1\\) iterations, with a time complexity of \\(O(n)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def access(head: ListNode, index: int) -> ListNode | None:\n    \"\"\"Access the node at index index in the linked list\"\"\"\n    for _ in range(index):\n        if not head:\n            return None\n        head = head.next\n    return head\n
    linked_list.cpp
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    ### Access node at index in linked list ###\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":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5-finding-a-node","level":3,"title":"5.   Finding a Node","text":"

    Traverse the linked list to find a node with value target, and output the index of that node in the linked list. This process is also a linear search. The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def find(head: ListNode, target: int) -> int:\n    \"\"\"Find the first node with value target in the linked list\"\"\"\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    ### Find first node with value target in linked list ###\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":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422-arrays-vs-linked-lists","level":2,"title":"4.2.2   Arrays vs. Linked Lists","text":"

    Table 4-1 summarizes the characteristics of arrays and linked lists and compares their operational efficiencies. Since they employ two opposite storage strategies, their various properties and operational efficiencies also exhibit contrasting characteristics.

    Table 4-1   Comparison of array and linked list efficiencies

    Array Linked List Storage method Contiguous memory space Scattered memory space Capacity expansion Immutable length Flexible expansion Memory efficiency Elements occupy less memory, but space may be wasted Elements occupy more memory Accessing an element \\(O(1)\\) \\(O(n)\\) Adding an element \\(O(n)\\) \\(O(1)\\) Removing an element \\(O(n)\\) \\(O(1)\\)","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423-common-types-of-linked-lists","level":2,"title":"4.2.3   Common Types of Linked Lists","text":"

    As shown in Figure 4-8, there are three common types of linked lists:

    • Singly linked list: This is the ordinary linked list introduced earlier. The nodes of a singly linked list contain a value and a reference to the next node. We call the first node the head node and the last node the tail node; the tail node points to None.
    • Circular linked list: If we make the tail node of a singly linked list point to the head node (connecting the tail to the head), we get a circular linked list. In a circular linked list, any node can be viewed as the head node.
    • Doubly linked list: Compared to a singly linked list, a doubly linked list records references in both directions. The node definition of a doubly linked list includes references to both the successor node (next node) and the predecessor node (previous node). Compared to a singly linked list, a doubly linked list is more flexible and can traverse the linked list in both directions, but it also requires more memory space.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class ListNode:\n    \"\"\"Doubly linked list node class\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Node value\n        self.next: ListNode | None = None  # Reference to the successor node\n        self.prev: ListNode | None = None  # Reference to the predecessor node\n
    /* Doubly linked list node structure */\nstruct ListNode {\n    int val;         // Node value\n    ListNode *next;  // Pointer to the successor node\n    ListNode *prev;  // Pointer to the predecessor node\n    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}  // Constructor\n};\n
    /* Doubly linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode next;  // Reference to the successor node\n    ListNode prev;  // Reference to the predecessor node\n    ListNode(int x) { val = x; }  // Constructor\n}\n
    /* Doubly linked list node class */\nclass ListNode(int x) {  // Constructor\n    int val = x;    // Node value\n    ListNode next;  // Reference to the successor node\n    ListNode prev;  // Reference to the predecessor node\n}\n
    /* Doubly linked list node structure */\ntype DoublyListNode struct {\n    Val  int             // Node value\n    Next *DoublyListNode // Pointer to the successor node\n    Prev *DoublyListNode // Pointer to the predecessor node\n}\n\n// NewDoublyListNode Initialization\nfunc NewDoublyListNode(val int) *DoublyListNode {\n    return &DoublyListNode{\n        Val:  val,\n        Next: nil,\n        Prev: nil,\n    }\n}\n
    /* Doubly linked list node class */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Reference to the successor node\n    var prev: ListNode? // Reference to the predecessor node\n\n    init(x: Int) { // Constructor\n        val = x\n    }\n}\n
    /* Doubly linked list node class */\nclass ListNode {\n    constructor(val, next, prev) {\n        this.val = val  ===  undefined ? 0 : val;        // Node value\n        this.next = next  ===  undefined ? null : next;  // Reference to the successor node\n        this.prev = prev  ===  undefined ? null : prev;  // Reference to the predecessor node\n    }\n}\n
    /* Doubly linked list node class */\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;        // Node value\n        this.next = next  ===  undefined ? null : next;  // Reference to the successor node\n        this.prev = prev  ===  undefined ? null : prev;  // Reference to the predecessor node\n    }\n}\n
    /* Doubly linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode? next;  // Reference to the successor node\n    ListNode? prev;  // Reference to the predecessor node\n    ListNode(this.val, [this.next, this.prev]);  // Constructor\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Doubly linked list node type */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Node value\n    next: Option<Rc<RefCell<ListNode>>>, // Pointer to the successor node\n    prev: Option<Rc<RefCell<ListNode>>>, // Pointer to the predecessor node\n}\n\n/* Constructor */\nimpl ListNode {\n    fn new(val: i32) -> Self {\n        ListNode {\n            val,\n            next: None,\n            prev: None,\n        }\n    }\n}\n
    /* Doubly linked list node structure */\ntypedef struct ListNode {\n    int val;               // Node value\n    struct ListNode *next; // Pointer to the successor node\n    struct ListNode *prev; // Pointer to the predecessor node\n} ListNode;\n\n/* Constructor */\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
    /* Doubly linked list node class */\n// Constructor\nclass ListNode(x: Int) {\n    val _val: Int = x           // Node value\n    val next: ListNode? = null  // Reference to the successor node\n    val prev: ListNode? = null  // Reference to the predecessor node\n}\n
    # Doubly linked list node class\nclass ListNode\n  attr_accessor :val    # Node value\n  attr_accessor :next   # Reference to the successor node\n  attr_accessor :prev   # Reference to the predecessor node\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

    Figure 4-8   Common types of linked lists

    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424-typical-applications-of-linked-lists","level":2,"title":"4.2.4   Typical Applications of Linked Lists","text":"

    Singly linked lists are commonly used to implement stacks, queues, hash tables, and graphs.

    • Stacks and queues: When insertion and deletion operations both occur at one end of the linked list, it exhibits last-in-first-out characteristics, corresponding to a stack. When insertion operations occur at one end of the linked list and deletion operations occur at the other end, it exhibits first-in-first-out characteristics, corresponding to a queue.
    • Hash tables: Separate chaining is one of the mainstream solutions for resolving hash collisions. In this approach, all colliding elements are placed in a linked list.
    • Graphs: An adjacency list is a common way to represent a graph, where each vertex in the graph is associated with a linked list, and each element in the linked list represents another vertex connected to that vertex.

    Doubly linked lists are commonly used in scenarios where quick access to the previous and next elements is needed.

    • Advanced data structures: For example, in red-black trees and B-trees, we need to access the parent node of a node, which can be achieved by saving a reference to the parent node in the node, similar to a doubly linked list.
    • Browser history: In web browsers, when a user clicks the forward or backward button, the browser needs to know the previous and next web pages the user visited. The characteristics of doubly linked lists make this operation simple.
    • LRU algorithm: In cache eviction (LRU) algorithms, we need to quickly find the least recently used data and support quick addition and deletion of nodes. Using a doubly linked list is very suitable for this.

    Circular linked lists are commonly used in scenarios that require periodic operations, such as operating system resource scheduling.

    • Round-robin scheduling algorithm: In operating systems, round-robin scheduling is a common CPU scheduling algorithm that needs to cycle through a set of processes. Each process is assigned a time slice, and when the time slice expires, the CPU switches to the next process. This cyclic operation can be implemented using a circular linked list.
    • Data buffers: In some data buffer implementations, circular linked lists may also be used. For example, in audio and video players, the data stream may be divided into multiple buffer blocks and placed in a circular linked list to achieve seamless playback.
    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   List","text":"

    A list is an abstract data structure that represents an ordered collection of elements. It supports operations such as element access, modification, insertion, deletion, and traversal, without requiring users to worry about capacity limits. Lists can be implemented using linked lists or arrays.

    • A linked list can naturally be viewed as a list: it supports insertion, deletion, search, and update, and can grow flexibly as needed.
    • An array also supports insertion, deletion, search, and update, but because its length is fixed, it can only be regarded as a list with a capacity limit.

    When a list is implemented with an array, its fixed length makes it less practical. This is because we usually cannot determine in advance how much data we need to store, making it difficult to choose an appropriate capacity. If the capacity is too small, it may fail to meet our needs; if it is too large, memory space will be wasted.

    To solve this problem, we can use a dynamic array to implement a list. It inherits all the advantages of arrays while supporting dynamic resizing during program execution.

    In fact, the list types provided by the standard libraries of many programming languages are implemented with dynamic arrays, such as list in Python, ArrayList in Java, vector in C++, and List in C#. In the following discussion, we will treat \"list\" and \"dynamic array\" as equivalent concepts.

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431-common-list-operations","level":2,"title":"4.3.1   Common List Operations","text":"","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1-initialize-a-list","level":3,"title":"1.   Initialize a List","text":"

    We typically initialize a list in one of two ways: empty or with predefined values:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Initialize a list\n# Without initial values\nnums1: list[int] = []\n# With initial values\nnums: list[int] = [1, 3, 2, 5, 4]\n
    list.cpp
    /* Initialize a list */\n// Note that vector in C++ is equivalent to nums as described in this article\n// Without initial values\nvector<int> nums1;\n// With initial values\nvector<int> nums = { 1, 3, 2, 5, 4 };\n
    list.java
    /* Initialize a list */\n// Without initial values\nList<Integer> nums1 = new ArrayList<>();\n// With initial values (note that array elements should use the wrapper class Integer[] instead of int[])\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n
    list.cs
    /* Initialize a list */\n// Without initial values\nList<int> nums1 = [];\n// With initial values\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n
    list_test.go
    /* Initialize a list */\n// Without initial values\nnums1 := []int{}\n// With initial values\nnums := []int{1, 3, 2, 5, 4}\n
    list.swift
    /* Initialize a list */\n// Without initial values\nlet nums1: [Int] = []\n// With initial values\nvar nums = [1, 3, 2, 5, 4]\n
    list.js
    /* Initialize a list */\n// Without initial values\nconst nums1 = [];\n// With initial values\nconst nums = [1, 3, 2, 5, 4];\n
    list.ts
    /* Initialize a list */\n// Without initial values\nconst nums1: number[] = [];\n// With initial values\nconst nums: number[] = [1, 3, 2, 5, 4];\n
    list.dart
    /* Initialize a list */\n// Without initial values\nList<int> nums1 = [];\n// With initial values\nList<int> nums = [1, 3, 2, 5, 4];\n
    list.rs
    /* Initialize a list */\n// Without initial values\nlet nums1: Vec<i32> = Vec::new();\n// With initial values\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Initialize a list */\n// Without initial values\nvar nums1 = listOf<Int>()\n// With initial values\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n
    list.rb
    # Initialize a list\n# Without initial values\nnums1 = []\n# With initial values\nnums = [1, 3, 2, 5, 4]\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2-access-elements","level":3,"title":"2.   Access Elements","text":"

    Since a list is essentially an array, we can access and update elements in \\(O(1)\\) time complexity, which is very efficient.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Access an element\nnum: int = nums[1]  # Access element at index 1\n\n# Update an element\nnums[1] = 0    # Update element at index 1 to 0\n
    list.cpp
    /* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list.java
    /* Access an element */\nint num = nums.get(1);  // Access element at index 1\n\n/* Update an element */\nnums.set(1, 0);  // Update element at index 1 to 0\n
    list.cs
    /* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list_test.go
    /* Access an element */\nnum := nums[1]  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0     // Update element at index 1 to 0\n
    list.swift
    /* Access an element */\nlet num = nums[1] // Access element at index 1\n\n/* Update an element */\nnums[1] = 0 // Update element at index 1 to 0\n
    list.js
    /* Access an element */\nconst num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list.ts
    /* Access an element */\nconst num: number = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list.dart
    /* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list.rs
    /* Access an element */\nlet num: i32 = nums[1];  // Access element at index 1\n/* Update an element */\nnums[1] = 0;             // Update element at index 1 to 0\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Access an element */\nval num = nums[1]       // Access element at index 1\n/* Update an element */\nnums[1] = 0             // Update element at index 1 to 0\n
    list.rb
    # Access an element\nnum = nums[1] # Access element at index 1\n# Update an element\nnums[1] = 0 # Update element at index 1 to 0\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3-insert-and-delete-elements","level":3,"title":"3.   Insert and Delete Elements","text":"

    Compared to arrays, lists can freely add and delete elements. Adding an element at the end of a list has a time complexity of \\(O(1)\\), but inserting and deleting elements still have the same efficiency as arrays, with a time complexity of \\(O(n)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Clear the list\nnums.clear()\n\n# Add elements at the end\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# Insert an element in the middle\nnums.insert(3, 6)  # Insert number 6 at index 3\n\n# Delete an element\nnums.pop(3)        # Delete element at index 3\n
    list.cpp
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* Insert an element in the middle */\nnums.insert(nums.begin() + 3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.erase(nums.begin() + 3);      // Delete element at index 3\n
    list.java
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.add(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);  // Delete element at index 3\n
    list.cs
    /* Clear the list */\nnums.Clear();\n\n/* Add elements at the end */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* Insert an element in the middle */\nnums.Insert(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.RemoveAt(3);  // Delete element at index 3\n
    list_test.go
    /* Clear the list */\nnums = nil\n\n/* Add elements at the end */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* Insert an element in the middle */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // Insert number 6 at index 3\n\n/* Delete an element */\nnums = append(nums[:3], nums[4:]...) // Delete element at index 3\n
    list.swift
    /* Clear the list */\nnums.removeAll()\n\n/* Add elements at the end */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* Insert an element in the middle */\nnums.insert(6, at: 3) // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(at: 3) // Delete element at index 3\n
    list.js
    /* Clear the list */\nnums.length = 0;\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.splice(3, 0, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.splice(3, 1);  // Delete element at index 3\n
    list.ts
    /* Clear the list */\nnums.length = 0;\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.splice(3, 0, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.splice(3, 1);  // Delete element at index 3\n
    list.dart
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.insert(3, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.removeAt(3); // Delete element at index 3\n
    list.rs
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.insert(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);    // Delete element at index 3\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.add(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);  // Delete element at index 3\n
    list.rb
    # Clear the list\nnums.clear\n\n# Add elements at the end\nnums << 1\nnums << 3\nnums << 2\nnums << 5\nnums << 4\n\n# Insert an element in the middle\nnums.insert(3, 6) # Insert number 6 at index 3\n\n# Delete an element\nnums.delete_at(3) # Delete element at index 3\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4-traverse-a-list","level":3,"title":"4.   Traverse a List","text":"

    Like arrays, lists can be traversed by index or by directly iterating through elements.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Traverse the list by index\ncount = 0\nfor i in range(len(nums)):\n    count += nums[i]\n\n# Traverse list elements directly\nfor num in nums:\n    count += num\n
    list.cpp
    /* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (int num : nums) {\n    count += num;\n}\n
    list.java
    /* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums.get(i);\n}\n\n/* Traverse list elements directly */\nfor (int num : nums) {\n    count += num;\n}\n
    list.cs
    /* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nforeach (int num in nums) {\n    count += num;\n}\n
    list_test.go
    /* Traverse the list by index */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\ncount = 0\nfor _, num := range nums {\n    count += num\n}\n
    list.swift
    /* Traverse the list by index */\nvar count = 0\nfor i in nums.indices {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\ncount = 0\nfor num in nums {\n    count += num\n}\n
    list.js
    /* Traverse the list by index */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
    list.ts
    /* Traverse the list by index */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
    list.dart
    /* Traverse the list by index */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (var num in nums) {\n    count += num;\n}\n
    list.rs
    // Traverse the list by index\nlet mut _count = 0;\nfor i in 0..nums.len() {\n    _count += nums[i];\n}\n\n// Traverse list elements directly\n_count = 0;\nfor num in &nums {\n    _count += num;\n}\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Traverse the list by index */\nvar count = 0\nfor (i in nums.indices) {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\nfor (num in nums) {\n    count += num\n}\n
    list.rb
    # Traverse the list by index\ncount = 0\nfor i in 0...nums.length\n    count += nums[i]\nend\n\n# Traverse list elements directly\ncount = 0\nfor num in nums\n    count += num\nend\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5-concatenate-lists","level":3,"title":"5.   Concatenate Lists","text":"

    Given a new list nums1, we can concatenate it to the end of the original list.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Concatenate two lists\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # Concatenate list nums1 to the end of nums\n
    list.cpp
    /* Concatenate two lists */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// Concatenate list nums1 to the end of nums\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
    list.java
    /* Concatenate two lists */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // Concatenate list nums1 to the end of nums\n
    list.cs
    /* Concatenate two lists */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // Concatenate list nums1 to the end of nums\n
    list_test.go
    /* Concatenate two lists */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // Concatenate list nums1 to the end of nums\n
    list.swift
    /* Concatenate two lists */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // Concatenate list nums1 to the end of nums\n
    list.js
    /* Concatenate two lists */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Concatenate list nums1 to the end of nums\n
    list.ts
    /* Concatenate two lists */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Concatenate list nums1 to the end of nums\n
    list.dart
    /* Concatenate two lists */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // Concatenate list nums1 to the end of nums\n
    list.rs
    /* Concatenate two lists */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Concatenate two lists */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // Concatenate list nums1 to the end of nums\n
    list.rb
    # Concatenate two lists\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6-sort-a-list","level":3,"title":"6.   Sort a List","text":"

    After sorting a list, we can use \"binary search\" and \"two-pointer\" algorithms, which are frequently tested in array algorithm problems.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Sort a list\nnums.sort()  # After sorting, list elements are arranged from smallest to largest\n
    list.cpp
    /* Sort a list */\nsort(nums.begin(), nums.end());  // After sorting, list elements are arranged from smallest to largest\n
    list.java
    /* Sort a list */\nCollections.sort(nums);  // After sorting, list elements are arranged from smallest to largest\n
    list.cs
    /* Sort a list */\nnums.Sort(); // After sorting, list elements are arranged from smallest to largest\n
    list_test.go
    /* Sort a list */\nsort.Ints(nums)  // After sorting, list elements are arranged from smallest to largest\n
    list.swift
    /* Sort a list */\nnums.sort() // After sorting, list elements are arranged from smallest to largest\n
    list.js
    /* Sort a list */\nnums.sort((a, b) => a - b);  // After sorting, list elements are arranged from smallest to largest\n
    list.ts
    /* Sort a list */\nnums.sort((a, b) => a - b);  // After sorting, list elements are arranged from smallest to largest\n
    list.dart
    /* Sort a list */\nnums.sort(); // After sorting, list elements are arranged from smallest to largest\n
    list.rs
    /* Sort a list */\nnums.sort(); // After sorting, list elements are arranged from smallest to largest\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Sort a list */\nnums.sort() // After sorting, list elements are arranged from smallest to largest\n
    list.rb
    # Sort a list\nnums = nums.sort { |a, b| a <=> b } # After sorting, list elements are arranged from smallest to largest\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432-list-implementation","level":2,"title":"4.3.2   List Implementation","text":"

    Many programming languages have built-in lists, such as Java, C++, and Python. Their implementations are quite complex, and the parameters are carefully considered, such as initial capacity, expansion multiples, and so on. Interested readers can consult the source code to learn more.

    To deepen our understanding of how lists work, we attempt to implement a simple list with three key design considerations:

    • Initial capacity: Select a reasonable initial capacity for the underlying array. In this example, we choose 10 as the initial capacity.
    • Size tracking: Declare a variable size to record the current number of elements in the list and update it in real-time as elements are inserted and deleted. Based on this variable, we can locate the end of the list and determine whether expansion is needed.
    • Expansion mechanism: When the list capacity is full upon inserting an element, we need to expand. We create a larger array based on the expansion multiple and then move all elements from the current array to the new array in order. In this example, we specify that the array should be expanded to 2 times its previous size each time.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py
    class MyList:\n    \"\"\"List class\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._capacity: int = 10  # List capacity\n        self._arr: list[int] = [0] * self._capacity  # Array (stores list elements)\n        self._size: int = 0  # List length (current number of elements)\n        self._extend_ratio: int = 2  # Multiple by which the list capacity is extended each time\n\n    def size(self) -> int:\n        \"\"\"Get list length (current number of elements)\"\"\"\n        return self._size\n\n    def capacity(self) -> int:\n        \"\"\"Get list capacity\"\"\"\n        return self._capacity\n\n    def get(self, index: int) -> int:\n        \"\"\"Access element\"\"\"\n        # If the index is out of bounds, throw an exception, as below\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        return self._arr[index]\n\n    def set(self, num: int, index: int):\n        \"\"\"Update element\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        self._arr[index] = num\n\n    def add(self, num: int):\n        \"\"\"Add element at the end\"\"\"\n        # When the number of elements exceeds capacity, trigger the extension mechanism\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        \"\"\"Insert element in the middle\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        # When the number of elements exceeds capacity, trigger the extension mechanism\n        if self._size == self.capacity():\n            self.extend_capacity()\n        # Move all elements at and after index index backward by one position\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        # Update the number of elements\n        self._size += 1\n\n    def remove(self, index: int) -> int:\n        \"\"\"Remove element\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        num = self._arr[index]\n        # Move all elements after index index forward by one position\n        for j in range(index, self._size - 1):\n            self._arr[j] = self._arr[j + 1]\n        # Update the number of elements\n        self._size -= 1\n        # Return the removed element\n        return num\n\n    def extend_capacity(self):\n        \"\"\"Extend list capacity\"\"\"\n        # Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n        # Update list capacity\n        self._capacity = len(self._arr)\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return list with valid length\"\"\"\n        return self._arr[: self._size]\n
    my_list.cpp
    /* List class */\nclass MyList {\n  private:\n    int *arr;             // Array (stores list elements)\n    int arrCapacity = 10; // List capacity\n    int arrSize = 0;      // List length (current number of elements)\n    int extendRatio = 2;   // Multiple by which the list capacity is extended each time\n\n  public:\n    /* Constructor */\n    MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Destructor */\n    ~MyList() {\n        delete[] arr;\n    }\n\n    /* Get list length (current number of elements)*/\n    int size() {\n        return arrSize;\n    }\n\n    /* Get list capacity */\n    int capacity() {\n        return arrCapacity;\n    }\n\n    /* Update element */\n    int get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    void set(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    void add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size() == capacity())\n            extendCapacity();\n        arr[size()] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Sort list */\n    void insert(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size() == capacity())\n            extendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = size() - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Remove element */\n    int remove(int index) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        int num = arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for (int j = index; j < size() - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        arrSize--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    void extendCapacity() {\n        // Create a new array with length extendRatio times the original array\n        int newCapacity = capacity() * extendRatio;\n        int *tmp = arr;\n        arr = new int[newCapacity];\n        // Copy all elements from the original array to the new array\n        for (int i = 0; i < size(); i++) {\n            arr[i] = tmp[i];\n        }\n        // Free memory\n        delete[] tmp;\n        arrCapacity = newCapacity;\n    }\n\n    /* Convert list to Vector for printing */\n    vector<int> toVector() {\n        // Elements enqueue\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
    /* List class */\nclass MyList {\n    private int[] arr; // Array (stores list elements)\n    private int capacity = 10; // List capacity\n    private int size = 0; // List length (current number of elements)\n    private int extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    public MyList() {\n        arr = new int[capacity];\n    }\n\n    /* Get list length (current number of elements) */\n    public int size() {\n        return size;\n    }\n\n    /* Get list capacity */\n    public int capacity() {\n        return capacity;\n    }\n\n    /* Update element */\n    public int get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    public void set(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public void add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity();\n        arr[size] = num;\n        // Update the number of elements\n        size++;\n    }\n\n    /* Sort list */\n    public void insert(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = size - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        size++;\n    }\n\n    /* Remove element */\n    public int remove(int index) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        int num = arr[index];\n        // Move all elements after index forward by one position\n        for (int j = index; j < size - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public void extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = Arrays.copyOf(arr, capacity() * extendRatio);\n        // Add elements at the end\n        capacity = arr.length;\n    }\n\n    /* Convert list to array */\n    public int[] toArray() {\n        int size = size();\n        // Elements enqueue\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
    /* List class */\nclass MyList {\n    private int[] arr;           // Array (stores list elements)\n    private int arrCapacity = 10;    // List capacity\n    private int arrSize = 0;         // List length (current number of elements)\n    private readonly int extendRatio = 2;  // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    public MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Get list length (current number of elements) */\n    public int Size() {\n        return arrSize;\n    }\n\n    /* Get list capacity */\n    public int Capacity() {\n        return arrCapacity;\n    }\n\n    /* Update element */\n    public int Get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    public void Set(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public void Add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        arr[arrSize] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Sort list */\n    public void Insert(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = arrSize - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Remove element */\n    public int Remove(int index) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        int num = arr[index];\n        // Move all elements after index forward by one position\n        for (int j = index; j < arrSize - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        arrSize--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public void ExtendCapacity() {\n        // Create new array of length arrCapacity * extendRatio and copy original array to new array\n        Array.Resize(ref arr, arrCapacity * extendRatio);\n        // Add elements at the end\n        arrCapacity = arr.Length;\n    }\n\n    /* Convert list to array */\n    public int[] ToArray() {\n        // Elements enqueue\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
    /* List class */\ntype myList struct {\n    arrCapacity int\n    arr         []int\n    arrSize     int\n    extendRatio int\n}\n\n/* Constructor */\nfunc newMyList() *myList {\n    return &myList{\n        arrCapacity: 10,              // List capacity\n        arr:         make([]int, 10), // Array (stores list elements)\n        arrSize:     0,               // List length (current number of elements)\n        extendRatio: 2,               // Multiple by which the list capacity is extended each time\n    }\n}\n\n/* Get list length (current number of elements) */\nfunc (l *myList) size() int {\n    return l.arrSize\n}\n\n/* Get list capacity */\nfunc (l *myList) capacity() int {\n    return l.arrCapacity\n}\n\n/* Update element */\nfunc (l *myList) get(index int) int {\n    // If the index is out of bounds, throw an exception, as below\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    return l.arr[index]\n}\n\n/* Add elements at the end */\nfunc (l *myList) set(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    l.arr[index] = num\n}\n\n/* Direct traversal of list elements */\nfunc (l *myList) add(num int) {\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    l.arr[l.arrSize] = num\n    // Update the number of elements\n    l.arrSize++\n}\n\n/* Sort list */\nfunc (l *myList) insert(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    // Move all elements after index index forward by one position\n    for j := l.arrSize - 1; j >= index; j-- {\n        l.arr[j+1] = l.arr[j]\n    }\n    l.arr[index] = num\n    // Update the number of elements\n    l.arrSize++\n}\n\n/* Remove element */\nfunc (l *myList) remove(index int) int {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    num := l.arr[index]\n    // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n    for j := index; j < l.arrSize-1; j++ {\n        l.arr[j] = l.arr[j+1]\n    }\n    // Update the number of elements\n    l.arrSize--\n    // Return the removed element\n    return num\n}\n\n/* Driver Code */\nfunc (l *myList) extendCapacity() {\n    // Create a new array with length extendRatio times the original array and copy the original array to the new array\n    l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n    // Add elements at the end\n    l.arrCapacity = len(l.arr)\n}\n\n/* Return list with valid length */\nfunc (l *myList) toArray() []int {\n    // Elements enqueue\n    return l.arr[:l.arrSize]\n}\n
    my_list.swift
    /* List class */\nclass MyList {\n    private var arr: [Int] // Array (stores list elements)\n    private var _capacity: Int // List capacity\n    private var _size: Int // List length (current number of elements)\n    private let extendRatio: Int // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    init() {\n        _capacity = 10\n        _size = 0\n        extendRatio = 2\n        arr = Array(repeating: 0, count: _capacity)\n    }\n\n    /* Get list length (current number of elements) */\n    func size() -> Int {\n        _size\n    }\n\n    /* Get list capacity */\n    func capacity() -> Int {\n        _capacity\n    }\n\n    /* Update element */\n    func get(index: Int) -> Int {\n        // Throw error if index out of bounds, same below\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        return arr[index]\n    }\n\n    /* Add elements at the end */\n    func set(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        arr[index] = num\n    }\n\n    /* Direct traversal of list elements */\n    func add(num: Int) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if size() == capacity() {\n            extendCapacity()\n        }\n        arr[size()] = num\n        // Update the number of elements\n        _size += 1\n    }\n\n    /* Sort list */\n    func insert(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if size() == capacity() {\n            extendCapacity()\n        }\n        // Move all elements after index index forward by one position\n        for j in (index ..< size()).reversed() {\n            arr[j + 1] = arr[j]\n        }\n        arr[index] = num\n        // Update the number of elements\n        _size += 1\n    }\n\n    /* Remove element */\n    @discardableResult\n    func remove(index: Int) -> Int {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        let num = arr[index]\n        // Move all elements after index forward by one position\n        for j in index ..< (size() - 1) {\n            arr[j] = arr[j + 1]\n        }\n        // Update the number of elements\n        _size -= 1\n        // Return the removed element\n        return num\n    }\n\n    /* Driver Code */\n    func extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n        // Add elements at the end\n        _capacity = arr.count\n    }\n\n    /* Convert list to array */\n    func toArray() -> [Int] {\n        Array(arr.prefix(size()))\n    }\n}\n
    my_list.js
    /* List class */\nclass MyList {\n    #arr = new Array(); // Array (stores list elements)\n    #capacity = 10; // List capacity\n    #size = 0; // List length (current number of elements)\n    #extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    constructor() {\n        this.#arr = new Array(this.#capacity);\n    }\n\n    /* Get list length (current number of elements) */\n    size() {\n        return this.#size;\n    }\n\n    /* Get list capacity */\n    capacity() {\n        return this.#capacity;\n    }\n\n    /* Update element */\n    get(index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        return this.#arr[index];\n    }\n\n    /* Add elements at the end */\n    set(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        this.#arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    add(num) {\n        // If length equals capacity, need to expand\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Add new element to end of list\n        this.#arr[this.#size] = num;\n        this.#size++;\n    }\n\n    /* Sort list */\n    insert(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Move all elements after index index forward by one position\n        for (let j = this.#size - 1; j >= index; j--) {\n            this.#arr[j + 1] = this.#arr[j];\n        }\n        // Update the number of elements\n        this.#arr[index] = num;\n        this.#size++;\n    }\n\n    /* Remove element */\n    remove(index) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        let num = this.#arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for (let j = index; j < this.#size - 1; j++) {\n            this.#arr[j] = this.#arr[j + 1];\n        }\n        // Update the number of elements\n        this.#size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        this.#arr = this.#arr.concat(\n            new Array(this.capacity() * (this.#extendRatio - 1))\n        );\n        // Add elements at the end\n        this.#capacity = this.#arr.length;\n    }\n\n    /* Convert list to array */\n    toArray() {\n        let size = this.size();\n        // Elements enqueue\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
    /* List class */\nclass MyList {\n    private arr: Array<number>; // Array (stores list elements)\n    private _capacity: number = 10; // List capacity\n    private _size: number = 0; // List length (current number of elements)\n    private extendRatio: number = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    constructor() {\n        this.arr = new Array(this._capacity);\n    }\n\n    /* Get list length (current number of elements) */\n    public size(): number {\n        return this._size;\n    }\n\n    /* Get list capacity */\n    public capacity(): number {\n        return this._capacity;\n    }\n\n    /* Update element */\n    public get(index: number): number {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        return this.arr[index];\n    }\n\n    /* Add elements at the end */\n    public set(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        this.arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public add(num: number): void {\n        // If length equals capacity, need to expand\n        if (this._size === this._capacity) this.extendCapacity();\n        // Add new element to end of list\n        this.arr[this._size] = num;\n        this._size++;\n    }\n\n    /* Sort list */\n    public insert(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (this._size === this._capacity) {\n            this.extendCapacity();\n        }\n        // Move all elements after index index forward by one position\n        for (let j = this._size - 1; j >= index; j--) {\n            this.arr[j + 1] = this.arr[j];\n        }\n        // Update the number of elements\n        this.arr[index] = num;\n        this._size++;\n    }\n\n    /* Remove element */\n    public remove(index: number): number {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        let num = this.arr[index];\n        // Move all elements after index forward by one position\n        for (let j = index; j < this._size - 1; j++) {\n            this.arr[j] = this.arr[j + 1];\n        }\n        // Update the number of elements\n        this._size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public extendCapacity(): void {\n        // Create new array of length size and copy original array to new array\n        this.arr = this.arr.concat(\n            new Array(this.capacity() * (this.extendRatio - 1))\n        );\n        // Add elements at the end\n        this._capacity = this.arr.length;\n    }\n\n    /* Convert list to array */\n    public toArray(): number[] {\n        let size = this.size();\n        // Elements enqueue\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
    /* List class */\nclass MyList {\n  late List<int> _arr; // Array (stores list elements)\n  int _capacity = 10; // List capacity\n  int _size = 0; // List length (current number of elements)\n  int _extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n  /* Constructor */\n  MyList() {\n    _arr = List.filled(_capacity, 0);\n  }\n\n  /* Get list length (current number of elements) */\n  int size() => _size;\n\n  /* Get list capacity */\n  int capacity() => _capacity;\n\n  /* Update element */\n  int get(int index) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    return _arr[index];\n  }\n\n  /* Add elements at the end */\n  void set(int index, int _num) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    _arr[index] = _num;\n  }\n\n  /* Direct traversal of list elements */\n  void add(int _num) {\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (_size == _capacity) extendCapacity();\n    _arr[_size] = _num;\n    // Update the number of elements\n    _size++;\n  }\n\n  /* Sort list */\n  void insert(int index, int _num) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (_size == _capacity) extendCapacity();\n    // Move all elements after index index forward by one position\n    for (var j = _size - 1; j >= index; j--) {\n      _arr[j + 1] = _arr[j];\n    }\n    _arr[index] = _num;\n    // Update the number of elements\n    _size++;\n  }\n\n  /* Remove element */\n  int remove(int index) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    int _num = _arr[index];\n    // Move all elements after index forward by one position\n    for (var j = index; j < _size - 1; j++) {\n      _arr[j] = _arr[j + 1];\n    }\n    // Update the number of elements\n    _size--;\n    // Return the removed element\n    return _num;\n  }\n\n  /* Driver Code */\n  void extendCapacity() {\n    // Create new array with length _extendRatio times original array\n    final _newNums = List.filled(_capacity * _extendRatio, 0);\n    // Copy original array to new array\n    List.copyRange(_newNums, 0, _arr);\n    // Update _arr reference\n    _arr = _newNums;\n    // Add elements at the end\n    _capacity = _arr.length;\n  }\n\n  /* Convert list to array */\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
    /* List class */\n#[allow(dead_code)]\nstruct MyList {\n    arr: Vec<i32>,       // Array (stores list elements)\n    capacity: usize,     // List capacity\n    size: usize,         // List length (current number of elements)\n    extend_ratio: usize, // Multiple by which the list capacity is extended each time\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n    /* Constructor */\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    /* Get list length (current number of elements) */\n    pub fn size(&self) -> usize {\n        return self.size;\n    }\n\n    /* Get list capacity */\n    pub fn capacity(&self) -> usize {\n        return self.capacity;\n    }\n\n    /* Update element */\n    pub fn get(&self, index: usize) -> i32 {\n        // If the index is out of bounds, throw an exception, as below\n        if index >= self.size {\n            panic!(\"Index out of bounds\")\n        };\n        return self.arr[index];\n    }\n\n    /* Add elements at the end */\n    pub fn set(&mut self, index: usize, num: i32) {\n        if index >= self.size {\n            panic!(\"Index out of bounds\")\n        };\n        self.arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    pub fn add(&mut self, num: i32) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        self.arr[self.size] = num;\n        // Update the number of elements\n        self.size += 1;\n    }\n\n    /* Sort list */\n    pub fn insert(&mut self, index: usize, num: i32) {\n        if index >= self.size() {\n            panic!(\"Index out of bounds\")\n        };\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        // Move all elements after index index forward by one position\n        for j in (index..self.size).rev() {\n            self.arr[j + 1] = self.arr[j];\n        }\n        self.arr[index] = num;\n        // Update the number of elements\n        self.size += 1;\n    }\n\n    /* Remove element */\n    pub fn remove(&mut self, index: usize) -> i32 {\n        if index >= self.size() {\n            panic!(\"Index out of bounds\")\n        };\n        let num = self.arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for j in index..self.size - 1 {\n            self.arr[j] = self.arr[j + 1];\n        }\n        // Update the number of elements\n        self.size -= 1;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    pub fn extend_capacity(&mut self) {\n        // Create new array with length extend_ratio times original, copy original array to new array\n        let new_capacity = self.capacity * self.extend_ratio;\n        self.arr.resize(new_capacity, 0);\n        // Add elements at the end\n        self.capacity = new_capacity;\n    }\n\n    /* Convert list to array */\n    pub fn to_array(&self) -> Vec<i32> {\n        // Elements enqueue\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
    /* List class */\ntypedef struct {\n    int *arr;        // Array (stores list elements)\n    int capacity;    // List capacity\n    int size;        // List size\n    int extendRatio; // List expansion multiplier\n} MyList;\n\n/* Constructor */\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/* Destructor */\nvoid delMyList(MyList *nums) {\n    free(nums->arr);\n    free(nums);\n}\n\n/* Get list length */\nint size(MyList *nums) {\n    return nums->size;\n}\n\n/* Get list capacity */\nint capacity(MyList *nums) {\n    return nums->capacity;\n}\n\n/* Update element */\nint get(MyList *nums, int index) {\n    assert(index >= 0 && index < nums->size);\n    return nums->arr[index];\n}\n\n/* Add elements at the end */\nvoid set(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < nums->size);\n    nums->arr[index] = num;\n}\n\n/* Direct traversal of list elements */\nvoid add(MyList *nums, int num) {\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Expand capacity\n    }\n    nums->arr[size(nums)] = num;\n    nums->size++;\n}\n\n/* Sort list */\nvoid insert(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < size(nums));\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Expand capacity\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/* Remove element */\n// Note: stdio.h occupies the remove keyword\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/* Driver Code */\nvoid extendCapacity(MyList *nums) {\n    // Allocate space first\n    int newCapacity = capacity(nums) * nums->extendRatio;\n    int *extend = (int *)malloc(sizeof(int) * newCapacity);\n    int *temp = nums->arr;\n\n    // Copy old data to new data\n    for (int i = 0; i < size(nums); i++)\n        extend[i] = nums->arr[i];\n\n    // Free old data\n    free(temp);\n\n    // Update new data\n    nums->arr = extend;\n    nums->capacity = newCapacity;\n}\n\n/* Convert list to Array for printing */\nint *toArray(MyList *nums) {\n    return nums->arr;\n}\n
    my_list.kt
    /* List class */\nclass MyList {\n    private var arr: IntArray = intArrayOf() // Array (stores list elements)\n    private var capacity: Int = 10 // List capacity\n    private var size: Int = 0 // List length (current number of elements)\n    private var extendRatio: Int = 2 // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    init {\n        arr = IntArray(capacity)\n    }\n\n    /* Get list length (current number of elements) */\n    fun size(): Int {\n        return size\n    }\n\n    /* Get list capacity */\n    fun capacity(): Int {\n        return capacity\n    }\n\n    /* Update element */\n    fun get(index: Int): Int {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        return arr[index]\n    }\n\n    /* Add elements at the end */\n    fun set(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        arr[index] = num\n    }\n\n    /* Direct traversal of list elements */\n    fun add(num: Int) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity()\n        arr[size] = num\n        // Update the number of elements\n        size++\n    }\n\n    /* Sort list */\n    fun insert(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity()\n        // Move all elements after index index forward by one position\n        for (j in size - 1 downTo index)\n            arr[j + 1] = arr[j]\n        arr[index] = num\n        // Update the number of elements\n        size++\n    }\n\n    /* Remove element */\n    fun remove(index: Int): Int {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        val num = arr[index]\n        // Move all elements after index forward by one position\n        for (j in index..<size - 1)\n            arr[j] = arr[j + 1]\n        // Update the number of elements\n        size--\n        // Return the removed element\n        return num\n    }\n\n    /* Driver Code */\n    fun extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = arr.copyOf(capacity() * extendRatio)\n        // Add elements at the end\n        capacity = arr.size\n    }\n\n    /* Convert list to array */\n    fun toArray(): IntArray {\n        val size = size()\n        // Elements enqueue\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
    ### List class ###\nclass MyList\n  attr_reader :size       # Get list length (current number of elements)\n  attr_reader :capacity   # Get list capacity\n\n  ### Constructor ###\n  def initialize\n    @capacity = 10\n    @size = 0\n    @extend_ratio = 2\n    @arr = Array.new(capacity)\n  end\n\n  ### Access element ###\n  def get(index)\n    # If the index is out of bounds, throw an exception, as below\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    @arr[index]\n  end\n\n  ### Access element ###\n  def set(index, num)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    @arr[index] = num\n  end\n\n  ### Add element at end ###\n  def add(num)\n    # When the number of elements exceeds capacity, trigger the extension mechanism\n    extend_capacity if size == capacity\n    @arr[size] = num\n\n    # Update the number of elements\n    @size += 1\n  end\n\n  ### Insert element in middle ###\n  def insert(index, num)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n\n    # When the number of elements exceeds capacity, trigger the extension mechanism\n    extend_capacity if size == capacity\n\n    # Move all elements after index index forward by one position\n    for j in (size - 1).downto(index)\n      @arr[j + 1] = @arr[j]\n    end\n    @arr[index] = num\n\n    # Update the number of elements\n    @size += 1\n  end\n\n  ### Delete element ###\n  def remove(index)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    num = @arr[index]\n\n    # Move all elements after index forward by one position\n    for j in index...size\n      @arr[j] = @arr[j + 1]\n    end\n\n    # Update the number of elements\n    @size -= 1\n\n    # Return the removed element\n    num\n  end\n\n  ### Expand list capacity ###\n  def extend_capacity\n    # Create new array with length extend_ratio times original, copy original array to new array\n    arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n    # Add elements at the end\n    @capacity = arr.length\n  end\n\n  ### Convert list to array ###\n  def to_array\n    sz = size\n    # Elements enqueue\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":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   Random-Access Memory and Cache *","text":"

    In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent two physical layouts: \"contiguous storage\" and \"distributed storage\", respectively.

    In fact, physical structure largely determines the efficiency with which programs utilize memory and cache, which in turn affects the overall performance of algorithmic programs.

    ","path":["Chapter 4. Arrays and Linked Lists","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441-computer-storage-devices","level":2,"title":"4.4.1   Computer Storage Devices","text":"

    Computers include three types of storage devices: hard disk, random-access memory (RAM), and cache memory. The following table shows their different roles and performance characteristics in a computer system.

    Table 4-2   Computer Storage Devices

    Hard Disk RAM Cache Purpose Long-term storage of data, including operating systems, programs, and files Temporary storage of currently running programs and data being processed Storage of frequently accessed data and instructions to reduce CPU's accesses to memory Volatility Data is not lost after power-off Data is lost after power-off Data is lost after power-off Capacity Large, on the order of terabytes (TB) Small, on the order of gigabytes (GB) Very small, on the order of megabytes (MB) Speed Slow, hundreds to thousands of MB/s Fast, tens of GB/s Very fast, tens to hundreds of GB/s Cost (CNY/GB) Inexpensive, from a few tenths of a yuan to a few yuan per GB Expensive, from tens to hundreds of yuan per GB Very expensive, effectively bundled with the CPU package

    We can imagine the computer storage system as a pyramid, as shown in the diagram below. Storage devices closer to the top are faster, have smaller capacity, and are more expensive. This multi-layered design is deliberate, the result of careful consideration by computer scientists and engineers.

    • Hard disk cannot be easily replaced by RAM. First, data in memory is lost after power-off, making it unsuitable for long-term data storage. Second, memory is tens of times more expensive than hard disk, which makes it difficult to popularize in the consumer market.
    • Cache cannot simultaneously achieve large capacity and high speed. As the capacity of L1, L2, and L3 caches increases, their physical size becomes larger, and the physical distance between them and the CPU core increases, resulting in longer data transmission time and higher element access latency. With current technology, the multi-layered cache structure represents the best balance point between capacity, speed, and cost.

    Figure 4-9   Computer Storage System

    Tip

    The storage hierarchy of computers embodies a delicate balance among speed, capacity, and cost. In fact, such trade-offs are common across all industrial fields, requiring us to find the optimal balance point between different advantages and constraints.

    In summary, hard disks are used for long-term storage of large amounts of data, RAM is used to temporarily store the data being processed during program execution, and cache is used to store frequently accessed data and instructions, thereby improving program execution efficiency. The three work together to keep the computer system running efficiently.

    As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU. By intelligently loading data from RAM, it provides the CPU with high-speed access to data, significantly improving program execution efficiency and reducing reliance on slower RAM.

    Figure 4-10   Data Flow Among Hard Disk, RAM, and Cache

    ","path":["Chapter 4. Arrays and Linked Lists","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442-memory-efficiency-of-data-structures","level":2,"title":"4.4.2   Memory Efficiency of Data Structures","text":"

    In terms of memory space utilization, arrays and linked lists each have advantages and limitations.

    On one hand, memory is limited, and the same memory cannot be shared by multiple programs, so we hope data structures can utilize space as efficiently as possible. Array elements are tightly packed and do not require additional space to store references (pointers) between linked list nodes, thus having higher space efficiency. However, arrays need to allocate sufficient contiguous memory space at once, which may lead to memory waste, and array expansion requires additional time and space costs. In comparison, linked lists perform dynamic memory allocation and deallocation on a \"node\" basis, providing greater flexibility.

    On the other hand, during program execution, as memory is repeatedly allocated and freed, the degree of fragmentation of free memory becomes increasingly severe, leading to reduced memory utilization efficiency. Arrays, due to their contiguous storage approach, are relatively less prone to memory fragmentation. Conversely, linked list elements are distributed in storage, and frequent insertion and deletion operations are more likely to cause memory fragmentation.

    ","path":["Chapter 4. Arrays and Linked Lists","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443-cache-efficiency-of-data-structures","level":2,"title":"4.4.3   Cache Efficiency of Data Structures","text":"

    Although cache has much smaller space capacity than memory, it is much faster than memory and plays a crucial role in program execution speed. Since cache capacity is limited and can only store a small portion of frequently accessed data, when the CPU attempts to access data that is not in the cache, a cache miss occurs, and the CPU must load the required data from the slower memory.

    Clearly, the fewer \"cache misses,\" the higher the efficiency of CPU data reads and writes, and the better the program performance. We call the proportion of data that the CPU successfully obtains from the cache the cache hit rate, a metric typically used to measure cache efficiency.

    To achieve the highest efficiency possible, cache employs the following data loading mechanisms.

    • Cache lines: The cache does not store and load data on a byte-by-byte basis, but rather as cache lines. Compared to byte-by-byte transmission, cache line transmission is more efficient.
    • Prefetching mechanism: The processor attempts to predict data access patterns (e.g., sequential access, fixed-stride jumping access, etc.) and loads data into the cache according to specific patterns, thereby improving hit rate.
    • Spatial locality: If a piece of data is accessed, nearby data may also be accessed in the near future. Therefore, when the cache loads a particular piece of data, it also loads nearby data to improve hit rate.
    • Temporal locality: If a piece of data is accessed, it is likely to be accessed again in the near future. Cache leverages this principle by retaining recently accessed data to improve hit rate.

    In fact, arrays and linked lists differ in how efficiently they utilize cache, mainly in the following respects.

    • Space occupied: Linked-list elements occupy more space than array elements, so less useful data can fit in the cache.
    • Cache lines: Linked list data are scattered throughout memory, while cache loads \"by lines,\" so the proportion of invalid data loaded is higher.
    • Prefetching mechanism: Arrays have more \"predictable\" data access patterns than linked lists, making it easier for the system to guess which data will be loaded next.
    • Spatial locality: Arrays are stored in centralized memory space, so data near loaded data is more likely to be accessed soon.

    Overall, arrays have higher cache hit rates, thus they usually outperform linked lists in operation efficiency. This makes data structures implemented based on arrays more popular when solving algorithmic problems.

    It is important to note that high cache efficiency does not mean arrays are superior to linked lists in all cases. In practical applications, which data structure to choose should be determined based on specific requirements. For example, both arrays and linked lists can implement the \"stack\" data structure (which will be discussed in detail in the next chapter), but they are suitable for different scenarios.

    • When solving algorithm problems, we tend to prefer stack implementations based on arrays, because they provide higher operation efficiency and the ability of random access, at the cost of needing to pre-allocate a certain amount of memory space for the array.
    • If the data volume is very large, the dynamic nature is high, and the expected size of the stack is difficult to estimate, then a stack implementation based on linked lists is more suitable. Linked lists can distribute large amounts of data across different parts of memory and avoid the additional overhead produced by array expansion.
    ","path":["Chapter 4. Arrays and Linked Lists","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   Summary","text":"","path":["Chapter 4. Arrays and Linked Lists","4.5   Summary"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous storage and scattered storage. Their strengths and weaknesses complement each other.
    • Arrays support random access and use less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization.
    • Linked lists achieve efficient insertion and deletion of nodes by modifying references (pointers), and can flexibly adjust length; however, node access is inefficient and memory consumption is higher. Common linked list types include singly linked lists, circular linked lists, and doubly linked lists.
    • A list is an ordered collection of elements that supports insertion, deletion, search, and modification, typically implemented based on dynamic arrays. It retains the advantages of arrays while allowing flexible adjustment of length.
    • The emergence of lists has greatly improved the practicality of arrays, but it may also waste some memory space.
    • During program execution, data is primarily stored in memory. Arrays provide higher memory space efficiency, while linked lists offer greater flexibility in memory usage.
    • Caches provide fast data access to the CPU through mechanisms such as cache lines, prefetching, and spatial and temporal locality, significantly improving program execution efficiency.
    • Because arrays have higher cache hit rates, they are generally more efficient than linked lists. When choosing a data structure, appropriate selection should be made based on specific requirements and scenarios.
    ","path":["Chapter 4. Arrays and Linked Lists","4.5   Summary"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Does storing an array on the stack versus on the heap affect time efficiency and space efficiency?

    Arrays stored on the stack and on the heap are both stored in contiguous memory space, so data operation efficiency is basically the same. However, the stack and heap have their own characteristics, leading to the following differences.

    1. Allocation and deallocation efficiency: The stack is a relatively small piece of memory, with allocation automatically handled by the compiler; the heap is relatively larger and can be dynamically allocated in code, more prone to fragmentation. Therefore, allocation and deallocation operations on the heap are usually slower than on the stack.
    2. Size limitations: Stack memory is relatively small, and the heap size is generally limited by available memory. Therefore, the heap is more suitable for storing large arrays.
    3. Flexibility: The size of an array on the stack must be determined at compile time, while the size of an array on the heap can be determined dynamically at runtime.

    Q: Why do arrays require elements of the same type, while linked lists do not emphasize this requirement?

    Linked lists are composed of nodes, with nodes connected through references (pointers), and each node can store different types of data, such as int, double, string, object, etc.

    In contrast, array elements must be of the same type so that their positions can be determined by calculating offsets. For example, if an array contains both int and long types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different \"element sizes\".

    # element address = array base address (address of the first element) + element size * element index\n

    Q: After deleting node P, do we need to set P.next to None?

    It is not necessary to modify P.next. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter P. This means that node P has been removed from the linked list, and it doesn't matter where node P points to at this time—it won't affect the linked list.

    From an algorithms-and-problem-solving perspective, leaving the pointer connected is fine as long as the program logic is correct. From a standard-library implementation perspective, explicitly disconnecting it is safer and clearer. If it is not disconnected and the deleted node is not reclaimed properly, it may affect the reclamation of successor nodes.

    Q: In a linked list, the time complexity of insertion and deletion operations is \\(O(1)\\). However, both insertion and deletion require \\(O(n)\\) time to find the element; why isn't the time complexity \\(O(n)\\)?

    If the element is first found and then deleted, the time complexity is indeed \\(O(n)\\). However, the advantage of \\(O(1)\\) insertion and deletion in linked lists can be demonstrated in other applications. For example, a deque is well-suited for linked list implementation, where we maintain pointer variables always pointing to the head and tail nodes, with each insertion and deletion operation being \\(O(1)\\).

    Q: In the diagram \"Linked List Definition and Storage Methods\", does the light blue pointer node occupy a single memory address, or does it share equally with the node value?

    This diagram is a qualitative representation; a quantitative representation requires analysis based on the specific situation.

    • Different types of node values occupy different amounts of space, such as int, long, double, and instance objects, etc.
    • The amount of memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes.

    Q: Is appending an element at the end of a list always \\(O(1)\\)?

    If appending an element exceeds the list length, the list must first be expanded before adding. The system allocates a new block of memory and moves all elements from the original list to it, in which case the time complexity becomes \\(O(n)\\).

    Q: \"The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space\"—does this space waste refer to the memory occupied by additional variables such as capacity, length, and expansion factor?

    This space waste mainly has two aspects: on one hand, lists typically set an initial length, which we may not need to fully utilize; on the other hand, to prevent frequent expansion, expansion generally multiplies by a coefficient, such as \\(\\times 1.5\\). As a result, there will be many empty positions that we typically cannot completely fill.

    Q: In Python, after initializing n = [1, 2, 3], the addresses of these 3 elements are contiguous, but initializing m = [2, 1, 3] reveals that each element's id is not continuous; rather, they are the same as those in n. Since the addresses of these elements are not contiguous, is m still an array?

    If we replace list elements with linked list nodes n = [n1, n2, n3, n4, n5], usually these 5 node objects are also scattered throughout memory. However, given a list index, we can still obtain the node memory address in \\(O(1)\\) time, thereby accessing the corresponding node. This is because the array stores references to nodes, not the nodes themselves.

    Unlike many languages, numbers in Python are wrapped as objects, and lists store not the numbers themselves, but references to the numbers. Therefore, we find that the same numbers in two arrays have the same id, and the memory addresses of these numbers need not be contiguous.

    Q: C++ STL has std::list which has already implemented a doubly linked list, but it seems that some algorithm books don't use it directly. Is there a limitation?

    On one hand, we often prefer to use arrays for implementing algorithms and only use linked lists when necessary, mainly for two reasons.

    • Space overhead: Since each element requires two additional pointers (one for the previous element and one for the next element), std::list typically consumes more space than std::vector.
    • Cache unfriendliness: Since data is not stored contiguously, std::list has lower cache utilization. In general, std::vector has better performance.

    On the other hand, cases where linked lists are necessary mainly involve binary trees and graphs. Stacks and queues usually use the stack and queue provided by the programming language, rather than linked lists.

    Q: Does the operation res = [[0]] * n create a 2D list where each [0] is independent?

    No, they are not independent. In this 2D list, all the [0] are actually references to the same object. If we modify one element, we will find that all corresponding elements change accordingly.

    If we want each [0] in the 2D list to be independent, we can use res = [[0] for _ in range(n)] to achieve this. The principle of this approach is to initialize \\(n\\) independent [0] list objects.

    Q: Does the operation res = [0] * n create a list where each integer 0 is independent?

    In this list, all the integer zeros reference the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance.

    Although they all reference the same object, we can still modify each element in the list independently. This is because Python integers are \"immutable objects\". When we modify an element, we actually switch that element to reference a different object, rather than changing the original object itself.

    However, when list elements are \"mutable objects\" (such as lists, dictionaries, or class instances), modifying an element directly changes the object itself, and all elements referencing that object will have the same change.

    ","path":["Chapter 4. Arrays and Linked Lists","4.5   Summary"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"Chapter 13.   Backtracking","text":"

    Abstract

    We are like explorers in a maze, and may encounter difficulties on the path forward.

    The power of backtracking allows us to start over, keep trying, and eventually find the exit leading to light.

    ","path":["Chapter 13. Backtracking","Chapter 13.   Backtracking"],"tags":[]},{"location":"chapter_backtracking/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 13.1   Backtracking Algorithm
    • 13.2   Permutations Problem
    • 13.3   Subset-Sum Problem
    • 13.4   N-Queens Problem
    • 13.5   Summary
    ","path":["Chapter 13. Backtracking","Chapter 13.   Backtracking"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   Backtracking Algorithm","text":"

    The backtracking algorithm is a method for solving problems through exhaustive search. Its core idea is to start from an initial state and exhaustively search all possible solutions. When a correct solution is found, it is recorded. This process continues until a solution is found or all possible choices have been tried without finding a solution.

    The backtracking algorithm typically employs \"depth-first search\" to traverse the solution space. In the \"Binary Tree\" chapter, we mentioned that preorder, inorder, and postorder traversals all belong to depth-first search. Next, we will construct a backtracking problem using preorder traversal to progressively understand how the backtracking algorithm works.

    Example 1

    Given a binary tree, search and record all nodes with value \\(7\\), and return a list of these nodes.

    For this problem, we perform a preorder traversal of the tree and check whether the current node's value is \\(7\\). If it is, we add the node to the result list res. The relevant implementation is shown in the following figure and code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 1\"\"\"\n    if root is None:\n        return\n    if root.val == 7:\n        # Record solution\n        res.append(root)\n    pre_order(root.left)\n    pre_order(root.right)\n
    preorder_traversal_i_compact.cpp
    /* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(root);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
    preorder_traversal_i_compact.java
    /* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Record solution\n        res.add(root);\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n}\n
    preorder_traversal_i_compact.cs
    /* Preorder traversal: Example 1 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Record solution\n        res.Add(root);\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n
    preorder_traversal_i_compact.go
    /* Preorder traversal: Example 1 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    if (root.Val).(int) == 7 {\n        // Record solution\n        *res = append(*res, root)\n    }\n    preOrderI(root.Left, res)\n    preOrderI(root.Right, res)\n}\n
    preorder_traversal_i_compact.swift
    /* Preorder traversal: Example 1 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    if root.val == 7 {\n        // Record solution\n        res.append(root)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n
    preorder_traversal_i_compact.js
    /* Preorder traversal: Example 1 */\nfunction preOrder(root, res) {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Record solution\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
    preorder_traversal_i_compact.ts
    /* Preorder traversal: Example 1 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Record solution\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
    preorder_traversal_i_compact.dart
    /* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode? root, List<TreeNode> res) {\n  if (root == null) {\n    return;\n  }\n  if (root.val == 7) {\n    // Record solution\n    res.add(root);\n  }\n  preOrder(root.left, res);\n  preOrder(root.right, res);\n}\n
    preorder_traversal_i_compact.rs
    /* Preorder traversal: Example 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            // Record solution\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
    /* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    if (root->val == 7) {\n        // Record solution\n        res[resSize++] = root;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
    preorder_traversal_i_compact.kt
    /* Preorder traversal: Example 1 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(root)\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n}\n
    preorder_traversal_i_compact.rb
    ### Pre-order traversal: example 1 ###\ndef pre_order(root)\n  return unless root\n\n  # Record solution\n  $res << root if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\nend\n

    Figure 13-1   Search for nodes in preorder traversal

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311-attempt-and-backtrack","level":2,"title":"13.1.1   Attempt and Backtrack","text":"

    The reason it is called a backtracking algorithm is that it employs \"attempt\" and \"backtrack\" strategies when searching the solution space. When the algorithm encounters a state where it cannot continue forward or cannot find a solution that satisfies the constraints, it will undo the previous choice, return to a previous state, and try other possible choices.

    For Example 1, visiting each node represents an \"attempt\", while skipping over a leaf node or the return that brings the traversal back to the parent node represents a \"backtrack\".

    It is worth noting that backtracking is not limited to function returns alone. To illustrate this, let's extend Example 1 slightly.

    Example 2

    In a binary tree, search all nodes with value \\(7\\), and return the paths from the root node to these nodes.

    Based on the code from Example 1, we need to use a list path to record the path of visited nodes. When we reach a node with value \\(7\\), we copy path and add it to the result list res. After traversal is complete, res contains all the solutions. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 2\"\"\"\n    if root is None:\n        return\n    # Attempt\n    path.append(root)\n    if root.val == 7:\n        # Record solution\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Backtrack\n    path.pop()\n
    preorder_traversal_ii_compact.cpp
    /* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    // Attempt\n    path.push_back(root);\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    path.pop_back();\n}\n
    preorder_traversal_ii_compact.java
    /* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    // Attempt\n    path.add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Backtrack\n    path.remove(path.size() - 1);\n}\n
    preorder_traversal_ii_compact.cs
    /* Preorder traversal: Example 2 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    // Attempt\n    path.Add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Backtrack\n    path.RemoveAt(path.Count - 1);\n}\n
    preorder_traversal_ii_compact.go
    /* Preorder traversal: Example 2 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    // Attempt\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Record solution\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderII(root.Left, res, path)\n    preOrderII(root.Right, res, path)\n    // Backtrack\n    *path = (*path)[:len(*path)-1]\n}\n
    preorder_traversal_ii_compact.swift
    /* Preorder traversal: Example 2 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Attempt\n    path.append(root)\n    if root.val == 7 {\n        // Record solution\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Backtrack\n    path.removeLast()\n}\n
    preorder_traversal_ii_compact.js
    /* Preorder traversal: Example 2 */\nfunction preOrder(root, path, res) {\n    if (root === null) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
    preorder_traversal_ii_compact.ts
    /* Preorder traversal: Example 2 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    if (root === null) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
    preorder_traversal_ii_compact.dart
    /* Preorder traversal: Example 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  // Attempt\n  path.add(root);\n  if (root.val == 7) {\n    // Record solution\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Backtrack\n  path.removeLast();\n}\n
    preorder_traversal_ii_compact.rs
    /* Preorder traversal: Example 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        // Attempt\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Record solution\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        // Backtrack\n        path.pop();\n    }\n}\n
    preorder_traversal_ii_compact.c
    /* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    // Attempt\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Record solution\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    // Backtrack\n    pathSize--;\n}\n
    preorder_traversal_ii_compact.kt
    /* Preorder traversal: Example 2 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    // Attempt\n    path!!.add(root)\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Backtrack\n    path!!.removeAt(path!!.size - 1)\n}\n
    preorder_traversal_ii_compact.rb
    ### Pre-order traversal: example 2 ###\ndef pre_order(root)\n  return unless root\n\n  # Attempt\n  $path << root\n\n  # Record solution\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Backtrack\n  $path.pop\nend\n

    In each \"attempt\", we record the path by adding the current node to path; before \"backtracking\", we need to remove the node from path, to restore the state before this attempt.

    Observing the process shown in the following figure, we can understand attempt and backtrack as \"advance\" and \"undo\", two operations that are the reverse of each other.

    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 13-2   Attempt and backtrack

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312-pruning","level":2,"title":"13.1.2   Pruning","text":"

    Complex backtracking problems usually contain one or more constraints. Constraints can typically be used for \"pruning\".

    Example 3

    In a binary tree, search all nodes with value \\(7\\) and return the paths from the root node to these nodes, but require that the paths do not contain nodes with value \\(3\\).

    To satisfy the above constraints, we need to add pruning operations: during the search process, if we encounter a node with value \\(3\\), we return early and do not continue searching. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 3\"\"\"\n    # Pruning\n    if root is None or root.val == 3:\n        return\n    # Attempt\n    path.append(root)\n    if root.val == 7:\n        # Record solution\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Backtrack\n    path.pop()\n
    preorder_traversal_iii_compact.cpp
    /* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode *root) {\n    // Pruning\n    if (root == nullptr || root->val == 3) {\n        return;\n    }\n    // Attempt\n    path.push_back(root);\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    path.pop_back();\n}\n
    preorder_traversal_iii_compact.java
    /* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode root) {\n    // Pruning\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Attempt\n    path.add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Backtrack\n    path.remove(path.size() - 1);\n}\n
    preorder_traversal_iii_compact.cs
    /* Preorder traversal: Example 3 */\nvoid PreOrder(TreeNode? root) {\n    // Pruning\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Attempt\n    path.Add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Backtrack\n    path.RemoveAt(path.Count - 1);\n}\n
    preorder_traversal_iii_compact.go
    /* Preorder traversal: Example 3 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    // Pruning\n    if root == nil || root.Val == 3 {\n        return\n    }\n    // Attempt\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Record solution\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderIII(root.Left, res, path)\n    preOrderIII(root.Right, res, path)\n    // Backtrack\n    *path = (*path)[:len(*path)-1]\n}\n
    preorder_traversal_iii_compact.swift
    /* Preorder traversal: Example 3 */\nfunc preOrder(root: TreeNode?) {\n    // Pruning\n    guard let root = root, root.val != 3 else {\n        return\n    }\n    // Attempt\n    path.append(root)\n    if root.val == 7 {\n        // Record solution\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Backtrack\n    path.removeLast()\n}\n
    preorder_traversal_iii_compact.js
    /* Preorder traversal: Example 3 */\nfunction preOrder(root, path, res) {\n    // Pruning\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
    preorder_traversal_iii_compact.ts
    /* Preorder traversal: Example 3 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Pruning\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
    preorder_traversal_iii_compact.dart
    /* Preorder traversal: Example 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  // Attempt\n  path.add(root);\n  if (root.val == 7) {\n    // Record solution\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Backtrack\n  path.removeLast();\n}\n
    preorder_traversal_iii_compact.rs
    /* Preorder traversal: Example 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    // Pruning\n    if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n        return;\n    }\n    if let Some(node) = root {\n        // Attempt\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Record solution\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        // Backtrack\n        path.pop();\n    }\n}\n
    preorder_traversal_iii_compact.c
    /* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode *root) {\n    // Pruning\n    if (root == NULL || root->val == 3) {\n        return;\n    }\n    // Attempt\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Record solution\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    // Backtrack\n    pathSize--;\n}\n
    preorder_traversal_iii_compact.kt
    /* Preorder traversal: Example 3 */\nfun preOrder(root: TreeNode?) {\n    // Pruning\n    if (root == null || root._val == 3) {\n        return\n    }\n    // Attempt\n    path!!.add(root)\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Backtrack\n    path!!.removeAt(path!!.size - 1)\n}\n
    preorder_traversal_iii_compact.rb
    ### Pre-order traversal: example 3 ###\ndef pre_order(root)\n  # Pruning\n  return if !root || root.val == 3\n\n  # Attempt\n  $path.append(root)\n\n  # Record solution\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Backtrack\n  $path.pop\nend\n

    \"Pruning\" is a vivid term. As shown in the following figure, during the search process, we \"prune\" search branches that do not satisfy the constraints, avoiding many meaningless attempts and thus improving search efficiency.

    Figure 13-3   Pruning according to constraints

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313-framework-code","level":2,"title":"13.1.3   Framework Code","text":"

    Next, we attempt to extract a general framework centered on backtracking's \"attempt, backtrack, and pruning\" to improve code generality.

    In the following framework code, state represents the current state of the problem, and choices represents the choices available in the current state:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    def backtrack(state: State, choices: list[choice], res: list[state]):\n    \"\"\"Backtracking algorithm framework\"\"\"\n    # Check if it is a solution\n    if is_solution(state):\n        # Record the solution\n        record_solution(state, res)\n        # Stop searching\n        return\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: check if the choice is valid\n        if is_valid(state, choice):\n            # Attempt: make a choice and update the state\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice)\n
    /* Backtracking algorithm framework */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (Choice choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (Choice choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n    // Check if it is a solution\n    if (IsSolution(state)) {\n        // Record the solution\n        RecordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    foreach (Choice choice in choices) {\n        // Pruning: check if the choice is valid\n        if (IsValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            MakeChoice(state, choice);\n            Backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            UndoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n    // Check if it is a solution\n    if isSolution(state) {\n        // Record the solution\n        recordSolution(state, res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for _, choice := range choices {\n        // Pruning: check if the choice is valid\n        if isValid(state, choice) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n    // Check if it is a solution\n    if isSolution(state: state) {\n        // Record the solution\n        recordSolution(state: state, res: &res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if isValid(state: state, choice: choice) {\n            // Attempt: make a choice and update the state\n            makeChoice(state: &state, choice: choice)\n            backtrack(state: &state, choices: choices, res: &res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfunction backtrack(state, choices, res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (let choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (let choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n  // Check if it is a solution\n  if (isSolution(state)) {\n    // Record the solution\n    recordSolution(state, res);\n    // Stop searching\n    return;\n  }\n  // Traverse all choices\n  for (Choice choice in choices) {\n    // Pruning: check if the choice is valid\n    if (isValid(state, choice)) {\n      // Attempt: make a choice and update the state\n      makeChoice(state, choice);\n      backtrack(state, choices, res);\n      // Backtrack: undo the choice and restore to the previous state\n      undoChoice(state, choice);\n    }\n  }\n}\n
    /* Backtracking algorithm framework */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n    // Check if it is a solution\n    if is_solution(state) {\n        // Record the solution\n        record_solution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if is_valid(state, choice) {\n            // Attempt: make a choice and update the state\n            make_choice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res, numRes);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < numChoices; i++) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, &choices[i])) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, &choices[i]);\n            backtrack(state, choices, numChoices, res, numRes);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, &choices[i]);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
    ### Backtracking algorithm framework ###\ndef backtrack(state, choices, res)\n    # Check if it is a solution\n    if is_solution?(state)\n        # Record the solution\n        record_solution(state, res)\n        return\n    end\n\n    # Traverse all choices\n    for choice in choices\n        # Pruning: check if the choice is valid\n        if is_valid?(state, choice)\n            # Attempt: make a choice and update the state\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice)\n        end\n    end\nend\n

    Next, we solve Example 3 based on the framework code. The state state is the node traversal path, the choices choices are the left and right child nodes of the current node, and the result res is a list of paths:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py
    def is_solution(state: list[TreeNode]) -> bool:\n    \"\"\"Check if the current state is a solution\"\"\"\n    return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n    \"\"\"Record solution\"\"\"\n    res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n    \"\"\"Check if the choice is valid under the current state\"\"\"\n    return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Update state\"\"\"\n    state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Restore state\"\"\"\n    state.pop()\n\ndef backtrack(\n    state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n    \"\"\"Backtracking algorithm: Example 3\"\"\"\n    # Check if it is a solution\n    if is_solution(state):\n        # Record solution\n        record_solution(state, res)\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: check if the choice is valid\n        if is_valid(state, choice):\n            # Attempt: make choice, update state\n            make_choice(state, choice)\n            # Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res)\n            # Backtrack: undo choice, restore to previous state\n            undo_choice(state, choice)\n
    preorder_traversal_iii_template.cpp
    /* Check if the current state is a solution */\nbool isSolution(vector<TreeNode *> &state) {\n    return !state.empty() && state.back()->val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n    res.push_back(state);\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n    return choice != nullptr && choice->val != 3;\n}\n\n/* Update state */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.push_back(choice);\n}\n\n/* Restore state */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.pop_back();\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (TreeNode *choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            vector<TreeNode *> nextChoices{choice->left, choice->right};\n            backtrack(state, nextChoices, res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    preorder_traversal_iii_template.java
    /* Check if the current state is a solution */\nboolean isSolution(List<TreeNode> state) {\n    return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.add(new ArrayList<>(state));\n}\n\n/* Check if the choice is valid under the current state */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n    state.add(choice);\n}\n\n/* Restore state */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n    state.remove(state.size() - 1);\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (TreeNode choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, Arrays.asList(choice.left, choice.right), res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    preorder_traversal_iii_template.cs
    /* Check if the current state is a solution */\nbool IsSolution(List<TreeNode> state) {\n    return state.Count != 0 && state[^1].val == 7;\n}\n\n/* Record solution */\nvoid RecordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.Add(new List<TreeNode>(state));\n}\n\n/* Check if the choice is valid under the current state */\nbool IsValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid MakeChoice(List<TreeNode> state, TreeNode choice) {\n    state.Add(choice);\n}\n\n/* Restore state */\nvoid UndoChoice(List<TreeNode> state, TreeNode choice) {\n    state.RemoveAt(state.Count - 1);\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid Backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Check if it is a solution\n    if (IsSolution(state)) {\n        // Record solution\n        RecordSolution(state, res);\n    }\n    // Traverse all choices\n    foreach (TreeNode choice in choices) {\n        // Pruning: check if the choice is valid\n        if (IsValid(state, choice)) {\n            // Attempt: make choice, update state\n            MakeChoice(state, choice);\n            // Proceed to the next round of selection\n            Backtrack(state, [choice.left!, choice.right!], res);\n            // Backtrack: undo choice, restore to previous state\n            UndoChoice(state, choice);\n        }\n    }\n}\n
    preorder_traversal_iii_template.go
    /* Check if the current state is a solution */\nfunc isSolution(state *[]*TreeNode) bool {\n    return len(*state) != 0 && (*state)[len(*state)-1].Val == 7\n}\n\n/* Record solution */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n    *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* Check if the choice is valid under the current state */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n    return choice != nil && choice.Val != 3\n}\n\n/* Update state */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = append(*state, choice)\n}\n\n/* Restore state */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = (*state)[:len(*state)-1]\n}\n\n/* Backtracking algorithm: Example 3 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n    // Check if it is a solution\n    if isSolution(state) {\n        // Record solution\n        recordSolution(state, res)\n    }\n    // Traverse all choices\n    for _, choice := range *choices {\n        // Pruning: check if the choice is valid\n        if isValid(state, choice) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice)\n            // Proceed to the next round of selection\n            temp := make([]*TreeNode, 0)\n            temp = append(temp, choice.Left, choice.Right)\n            backtrackIII(state, &temp, res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
    preorder_traversal_iii_template.swift
    /* Check if the current state is a solution */\nfunc isSolution(state: [TreeNode]) -> Bool {\n    !state.isEmpty && state.last!.val == 7\n}\n\n/* Record solution */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n    res.append(state)\n}\n\n/* Check if the choice is valid under the current state */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -> Bool {\n    choice != nil && choice!.val != 3\n}\n\n/* Update state */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.append(choice)\n}\n\n/* Restore state */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.removeLast()\n}\n\n/* Backtracking algorithm: Example 3 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n    // Check if it is a solution\n    if isSolution(state: state) {\n        recordSolution(state: state, res: &res)\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if isValid(state: state, choice: choice) {\n            // Attempt: make choice, update state\n            makeChoice(state: &state, choice: choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
    preorder_traversal_iii_template.js
    /* Check if the current state is a solution */\nfunction isSolution(state) {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Record solution */\nfunction recordSolution(state, res) {\n    res.push([...state]);\n}\n\n/* Check if the choice is valid under the current state */\nfunction isValid(state, choice) {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Update state */\nfunction makeChoice(state, choice) {\n    state.push(choice);\n}\n\n/* Restore state */\nfunction undoChoice(state) {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfunction backtrack(state, choices, res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state);\n        }\n    }\n}\n
    preorder_traversal_iii_template.ts
    /* Check if the current state is a solution */\nfunction isSolution(state: TreeNode[]): boolean {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Record solution */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n    res.push([...state]);\n}\n\n/* Check if the choice is valid under the current state */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Update state */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n    state.push(choice);\n}\n\n/* Restore state */\nfunction undoChoice(state: TreeNode[]): void {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfunction backtrack(\n    state: TreeNode[],\n    choices: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state);\n        }\n    }\n}\n
    preorder_traversal_iii_template.dart
    /* Check if the current state is a solution */\nbool isSolution(List<TreeNode> state) {\n  return state.isNotEmpty && state.last.val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n  res.add(List.from(state));\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(List<TreeNode> state, TreeNode? choice) {\n  return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid makeChoice(List<TreeNode> state, TreeNode? choice) {\n  state.add(choice!);\n}\n\n/* Restore state */\nvoid undoChoice(List<TreeNode> state, TreeNode? choice) {\n  state.removeLast();\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(\n  List<TreeNode> state,\n  List<TreeNode?> choices,\n  List<List<TreeNode>> res,\n) {\n  // Check if it is a solution\n  if (isSolution(state)) {\n    // Record solution\n    recordSolution(state, res);\n  }\n  // Traverse all choices\n  for (TreeNode? choice in choices) {\n    // Pruning: check if the choice is valid\n    if (isValid(state, choice)) {\n      // Attempt: make choice, update state\n      makeChoice(state, choice);\n      // Proceed to the next round of selection\n      backtrack(state, [choice!.left, choice.right], res);\n      // Backtrack: undo choice, restore to previous state\n      undoChoice(state, choice);\n    }\n  }\n}\n
    preorder_traversal_iii_template.rs
    /* Check if the current state is a solution */\nfn is_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>) -> bool {\n    return !state.is_empty() && state.last().unwrap().borrow().val == 7;\n}\n\n/* Record solution */\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/* Check if the choice is valid under the current state */\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/* Update state */\nfn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {\n    state.push(choice);\n}\n\n/* Restore state */\nfn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 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    // Check if it is a solution\n    if is_solution(state) {\n        // Record solution\n        record_solution(state, res);\n    }\n    // Traverse all choices\n    for &choice in choices.iter() {\n        // Pruning: check if the choice is valid\n        if is_valid(state, choice) {\n            // Attempt: make choice, update state\n            make_choice(state, choice.unwrap().clone());\n            // Proceed to the next round of selection\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            // Backtrack: undo choice, restore to previous state\n            undo_choice(state, choice.unwrap().clone());\n        }\n    }\n}\n
    preorder_traversal_iii_template.c
    /* Check if the current state is a solution */\nbool isSolution(void) {\n    return pathSize > 0 && path[pathSize - 1]->val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(void) {\n    for (int i = 0; i < pathSize; i++) {\n        res[resSize][i] = path[i];\n    }\n    resSize++;\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(TreeNode *choice) {\n    return choice != NULL && choice->val != 3;\n}\n\n/* Update state */\nvoid makeChoice(TreeNode *choice) {\n    path[pathSize++] = choice;\n}\n\n/* Restore state */\nvoid undoChoice(void) {\n    pathSize--;\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(TreeNode *choices[2]) {\n    // Check if it is a solution\n    if (isSolution()) {\n        // Record solution\n        recordSolution();\n    }\n    // Traverse all choices\n    for (int i = 0; i < 2; i++) {\n        TreeNode *choice = choices[i];\n        // Pruning: check if the choice is valid\n        if (isValid(choice)) {\n            // Attempt: make choice, update state\n            makeChoice(choice);\n            // Proceed to the next round of selection\n            TreeNode *nextChoices[2] = {choice->left, choice->right};\n            backtrack(nextChoices);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice();\n        }\n    }\n}\n
    preorder_traversal_iii_template.kt
    /* Check if the current state is a solution */\nfun isSolution(state: MutableList<TreeNode?>): Boolean {\n    return state.isNotEmpty() && state[state.size - 1]?._val == 7\n}\n\n/* Record solution */\nfun recordSolution(state: MutableList<TreeNode?>?, res: MutableList<MutableList<TreeNode?>?>) {\n    res.add(state!!.toMutableList())\n}\n\n/* Check if the choice is valid under the current state */\nfun isValid(state: MutableList<TreeNode?>?, choice: TreeNode?): Boolean {\n    return choice != null && choice._val != 3\n}\n\n/* Update state */\nfun makeChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.add(choice)\n}\n\n/* Restore state */\nfun undoChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.removeLast()\n}\n\n/* Backtracking algorithm: Example 3 */\nfun backtrack(\n    state: MutableList<TreeNode?>,\n    choices: MutableList<TreeNode?>,\n    res: MutableList<MutableList<TreeNode?>?>\n) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res)\n    }\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice)\n            // Proceed to the next round of selection\n            backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
    preorder_traversal_iii_template.rb
    ### Check if current state is solution ###\ndef is_solution?(state)\n  !state.empty? && state.last.val == 7\nend\n\n### Record solution ###\ndef record_solution(state, res)\n  res << state.dup\nend\n\n### Check if choice is valid in current state ###\ndef is_valid?(state, choice)\n  choice && choice.val != 3\nend\n\n### Update state ###\ndef make_choice(state, choice)\n  state << choice\nend\n\n### Restore state ###\ndef undo_choice(state, choice)\n  state.pop\nend\n\n### Backtracking: example 3 ###\ndef backtrack(state, choices, res)\n  # Check if it is a solution\n  record_solution(state, res) if is_solution?(state)\n\n  # Traverse all choices\n  for choice in choices\n    # Pruning: check if the choice is valid\n    if is_valid?(state, choice)\n      # Attempt: make choice, update state\n      make_choice(state, choice)\n      # Proceed to the next round of selection\n      backtrack(state, [choice.left, choice.right], res)\n      # Backtrack: undo choice, restore to previous state\n      undo_choice(state, choice)\n    end\n  end\nend\n

    As per the problem statement, we should continue searching after finding a node with value \\(7\\). Therefore, we need to remove the return statement after recording the solution. The following figure compares the search process with and without the return statement.

    Figure 13-4   Comparison of search process with and without return statement

    Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but is more general. In fact, many backtracking problems can be solved within this framework. We only need to define state and choices for the specific problem and implement each method in the framework.

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314-common-terminology","level":2,"title":"13.1.4   Common Terminology","text":"

    To analyze algorithmic problems more clearly, we summarize the meanings of common terminology used in backtracking algorithms and provide corresponding examples from Example 3, as shown in the following table.

    Table 13-1   Common Backtracking Algorithm Terminology

    Term Definition Example 3 Solution (solution) A solution is an answer that satisfies the specific conditions of a problem; there may be one or more solutions All paths from root to nodes with value \\(7\\) that satisfy the constraint Constraint (constraint) A constraint is a condition in the problem that limits the feasibility of solutions, typically used for pruning Paths do not contain nodes with value \\(3\\) State (state) State represents the situation of a problem at a certain moment, including the choices already made The currently visited node path, i.e., the path list of nodes Attempt (attempt) An attempt is the process of exploring the solution space according to available choices, including making choices, updating state, and checking if it is a solution Recursively visit left (right) child nodes, add nodes to path, check if node value is \\(7\\) Backtrack (backtracking) Backtracking refers to undoing previous choices and returning to a previous state when encountering a state that does not satisfy constraints Stop searching when passing over leaf nodes, ending node visits, or encountering nodes with value \\(3\\); function returns Pruning (pruning) Pruning is a method of avoiding meaningless search paths according to problem characteristics and constraints, which can improve search efficiency When encountering a node with value \\(3\\), do not continue searching

    Tip

    The concepts of problem, solution, state, etc. are universal and appear in divide-and-conquer, backtracking, dynamic programming, greedy algorithms, and others.

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315-advantages-and-limitations","level":2,"title":"13.1.5   Advantages and Limitations","text":"

    The backtracking algorithm is essentially a depth-first search algorithm that tries all possible solutions until it finds one that satisfies the conditions. The advantage of this approach is that it can find all possible solutions, and with reasonable pruning operations, it achieves high efficiency.

    However, when dealing with large-scale or complex problems, the running efficiency of the backtracking algorithm may be unacceptable.

    • Time: The backtracking algorithm usually needs to traverse all possibilities in the state space, and the time complexity can reach exponential or factorial order.
    • Space: During recursive calls, the current state needs to be saved (such as paths, auxiliary variables used for pruning, etc.), and when the depth is large, the space requirement can become very large.

    Nevertheless, the backtracking algorithm is still the best solution for certain search problems and constraint satisfaction problems. For these problems, since we cannot predict which choices will generate valid solutions, we must traverse all possible choices. In this case, the key is how to optimize efficiency. There are two common efficiency optimization methods.

    • Pruning: Avoid searching paths that are guaranteed not to produce solutions, thereby saving time and space.
    • Heuristic search: Introduce certain strategies or estimation values during the search process to prioritize searching paths that are most likely to produce valid solutions.
    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316-typical-backtracking-examples","level":2,"title":"13.1.6   Typical Backtracking Examples","text":"

    The backtracking algorithm can be used to solve many search problems, constraint satisfaction problems, and combinatorial optimization problems.

    Search problems: The goal of these problems is to find solutions that satisfy specific conditions.

    • Permutation problem: Given a set, find all possible permutations and combinations.
    • Subset sum problem: Given a set and a target sum, find all subsets in the set whose elements sum to the target.
    • Tower of Hanoi: Given three pegs and a series of disks of different sizes, move all disks from one peg to another, moving only one disk at a time, and never placing a larger disk on a smaller disk.

    Constraint satisfaction problems: The goal of these problems is to find solutions that satisfy all constraints.

    • N-Queens: Place \\(n\\) queens on an \\(n \\times n\\) chessboard such that they do not attack each other.
    • Sudoku: Fill numbers \\(1\\) to \\(9\\) in a \\(9 \\times 9\\) grid such that each row, column, and \\(3 \\times 3\\) subgrid contains no repeated digits.
    • Graph coloring: Given an undirected graph, color each vertex with the minimum number of colors such that adjacent vertices have different colors.

    Combinatorial optimization problems: The goal of these problems is to find an optimal solution that satisfies certain conditions in a combinatorial space.

    • 0-1 Knapsack: Given a set of items and a knapsack, each item has a value and weight. Under the knapsack capacity constraint, select items to maximize total value.
    • Traveling Salesman Problem: Starting from a point in a graph, visit all other points exactly once and return to the starting point, finding the shortest path.
    • Maximum Clique: Given an undirected graph, find the largest complete subgraph, i.e., a subgraph where any two vertices are connected by an edge.

    Note that for many combinatorial optimization problems, backtracking is not the optimal solution.

    • The 0-1 Knapsack problem is usually solved using dynamic programming to achieve higher time efficiency.
    • The Traveling Salesman Problem is a famous NP-Hard problem; common solutions include genetic algorithms and ant colony algorithms.
    • The Maximum Clique problem is a classical problem in graph theory and can be solved using heuristic algorithms such as greedy algorithms.
    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   N-Queens Problem","text":"

    Question

    According to the rules of chess, a queen can attack any piece in the same row, column, or diagonal. Given \\(n\\) queens and an \\(n \\times n\\) chessboard, find an arrangement such that no two queens can attack each other.

    As shown in Figure 13-15, when \\(n = 4\\), there are two solutions that can be found. From the perspective of the backtracking algorithm, an \\(n \\times n\\) chessboard has \\(n^2\\) squares, which provide all the choices choices. During the process of placing queens one by one, the chessboard state changes continuously, and the chessboard at each moment represents the state state.

    Figure 13-15   Solution to the 4-queens problem

    Figure 13-16 illustrates the three constraints of this problem: multiple queens cannot be in the same row, the same column, or on the same diagonal. It is worth noting that diagonals are divided into two types: the main diagonal \\ and the anti-diagonal /.

    Figure 13-16   Constraints of the n-queens problem

    ","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1-row-by-row-placement-strategy","level":3,"title":"1.   Row-By-Row Placement Strategy","text":"

    Since both the number of queens and the number of rows on the chessboard are \\(n\\), we can easily derive a conclusion: each row of the chessboard allows one and only one queen to be placed.

    This means we can adopt a row-by-row placement strategy: starting from the first row, place one queen in each row until the last row is completed.

    Figure 13-17 shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that violate the column or diagonal constraints are pruned.

    Figure 13-17   Row-by-row placement strategy

    Essentially, the row-by-row placement strategy serves a pruning function, as it avoids all search branches where multiple queens appear in the same row.

    ","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2-column-and-diagonal-pruning","level":3,"title":"2.   Column and Diagonal Pruning","text":"

    To satisfy the column constraint, we can use a boolean array cols of length \\(n\\) to record whether each column has a queen. Before each placement decision, we use cols to prune columns that already have queens, and dynamically update the state of cols during backtracking.

    Tip

    Please note that the origin of the matrix is located in the upper-left corner, where the row index increases from top to bottom, and the column index increases from left to right.

    So how do we handle diagonal constraints? Consider a square on the chessboard with row and column indices \\((row, col)\\). If we select a specific main diagonal in the matrix, we find that all squares on that diagonal have the same difference between their row and column indices, meaning that \\(row - col\\) is a constant value for all squares on the main diagonal.

    In other words, if two squares satisfy \\(row_1 - col_1 = row_2 - col_2\\), they must be on the same main diagonal. Using this pattern, we can use the array diags1 shown in Figure 13-18 to record whether there is a queen on each main diagonal.

    Similarly, for all squares on an anti-diagonal, the sum \\(row + col\\) is a constant value. We can likewise use the array diags2 to handle anti-diagonal constraints.

    Figure 13-18   Handling column and diagonal constraints

    ","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

    Please note that in an \\(n \\times n\\) square matrix, the range of \\(row - col\\) is \\([-n + 1, n - 1]\\), and the range of \\(row + col\\) is \\([0, 2n - 2]\\). Therefore, the number of both main diagonals and anti-diagonals is \\(2n - 1\\), meaning the length of both arrays diags1 and diags2 is \\(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    \"\"\"Backtracking algorithm: N queens\"\"\"\n    # When all rows are placed, record the solution\n    if row == n:\n        res.append([list(row) for row in state])\n        return\n    # Traverse all columns\n    for col in range(n):\n        # Calculate the main diagonal and anti-diagonal corresponding to this cell\n        diag1 = row - col + n - 1\n        diag2 = row + col\n        # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n            # Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            cols[col] = diags1[diag1] = diags2[diag2] = True\n            # Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            # Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n    \"\"\"Solve N queens\"\"\"\n    # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    state = [[\"#\" for _ in range(n)] for _ in range(n)]\n    cols = [False] * n  # Record whether there is a queen in the column\n    diags1 = [False] * (2 * n - 1)  # Record whether there is a queen on the main diagonal\n    diags2 = [False] * (2 * n - 1)  # Record whether there is a queen on the anti-diagonal\n    res = []\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n
    n_queens.cpp
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\n    if (row == n) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nvector<vector<vector<string>>> nQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    vector<vector<string>> state(n, vector<string>(n, \"#\"));\n    vector<bool> cols(n, false);           // Record whether there is a queen in the column\n    vector<bool> diags1(2 * n - 1, false); // Record whether there is a queen on the main diagonal\n    vector<bool> diags2(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n        boolean[] cols, boolean[] diags1, boolean[] diags2) {\n    // When all rows are placed, record the solution\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    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state.get(row).set(col, \"Q\");\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state.get(row).set(col, \"#\");\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nList<List<List<String>>> nQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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]; // Record whether there is a queen in the column\n    boolean[] diags1 = new boolean[2 * n - 1]; // Record whether there is a queen on the main diagonal\n    boolean[] diags2 = new boolean[2 * n - 1]; // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\nvoid Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,\n        bool[] cols, bool[] diags1, bool[] diags2) {\n    // When all rows are placed, record the solution\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    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nList<List<List<string>>> NQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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]; // Record whether there is a queen in the column\n    bool[] diags1 = new bool[2 * n - 1]; // Record whether there is a queen on the main diagonal\n    bool[] diags2 = new bool[2 * n - 1]; // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n    // When all rows are placed, record the solution\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    // Traverse all columns\n    for col := 0; col < n; col++ {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        diag1 := row - col + n - 1\n        diag2 := row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {\n            // Attempt: place the queen in this cell\n            (*state)[row][col] = \"Q\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n            // Place the next row\n            backtrack(row+1, n, state, res, cols, diags1, diags2)\n            // Backtrack: restore this cell to an empty cell\n            (*state)[row][col] = \"#\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n        }\n    }\n}\n\n/* Solve N queens */\nfunc nQueens(n int) [][][]string {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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    // Record whether there is a queen in the column\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
    /* Backtracking algorithm: N queens */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n    // When all rows are placed, record the solution\n    if row == n {\n        res.append(state)\n        return\n    }\n    // Traverse all columns\n    for col in 0 ..< n {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        let diag1 = row - col + n - 1\n        let diag2 = row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            cols[col] = true\n            diags1[diag1] = true\n            diags2[diag2] = true\n            // Place the next row\n            backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            cols[col] = false\n            diags1[diag1] = false\n            diags2[diag2] = false\n        }\n    }\n}\n\n/* Solve N queens */\nfunc nQueens(n: Int) -> [[[String]]] {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n    var cols = Array(repeating: false, count: n) // Record whether there is a queen in the column\n    var diags1 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the main diagonal\n    var diags2 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n    // When all rows are placed, record the solution\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Traverse all columns\n    for (let col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nfunction nQueens(n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Record whether there is a queen in the column\n    const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal\n    const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal\n    const res = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
    n_queens.ts
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Traverse all columns\n    for (let col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nfunction nQueens(n: number): string[][][] {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Record whether there is a queen in the column\n    const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal\n    const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal\n    const res: string[][][] = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
    n_queens.dart
    /* Backtracking algorithm: N queens */\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  // When all rows are placed, record the solution\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  // Traverse all columns\n  for (int col = 0; col < n; col++) {\n    // Calculate the main diagonal and anti-diagonal corresponding to this cell\n    int diag1 = row - col + n - 1;\n    int diag2 = row + col;\n    // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n    if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n      // Attempt: place the queen in this cell\n      state[row][col] = \"Q\";\n      cols[col] = true;\n      diags1[diag1] = true;\n      diags2[diag2] = true;\n      // Place the next row\n      backtrack(row + 1, n, state, res, cols, diags1, diags2);\n      // Backtrack: restore this cell to an empty cell\n      state[row][col] = \"#\";\n      cols[col] = false;\n      diags1[diag1] = false;\n      diags2[diag2] = false;\n    }\n  }\n}\n\n/* Solve N queens */\nList<List<List<String>>> nQueens(int n) {\n  // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n  List<List<String>> state = List.generate(n, (index) => List.filled(n, \"#\"));\n  List<bool> cols = List.filled(n, false); // Record whether there is a queen in the column\n  List<bool> diags1 = List.filled(2 * n - 1, false); // Record whether there is a queen on the main diagonal\n  List<bool> diags2 = List.filled(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\n    if row == n {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all columns\n    for col in 0..n {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        let diag1 = row + n - 1 - col;\n        let diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n        }\n    }\n}\n\n/* Solve N queens */\nfn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    let mut state: Vec<Vec<String>> = vec![vec![\"#\".to_string(); n]; n];\n    let mut cols = vec![false; n]; // Record whether there is a queen in the column\n    let mut diags1 = vec![false; 2 * n - 1]; // Record whether there is a queen on the main diagonal\n    let mut diags2 = vec![false; 2 * n - 1]; // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\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    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nchar ***nQueens(int n, int *returnSize) {\n    char state[MAX_SIZE][MAX_SIZE];\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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};           // Record whether there is a queen in the column\n    bool diags1[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the main diagonal\n    bool diags2[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\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    // Traverse all columns\n    for (col in 0..<n) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        val diag1 = row - col + n - 1\n        val diag2 = row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            diags2[diag2] = true\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            diags2[diag2] = false\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n        }\n    }\n}\n\n/* Solve N queens */\nfun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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) // Record whether there is a queen in the column\n    val diags1 = BooleanArray(2 * n - 1) // Record whether there is a queen on the main diagonal\n    val diags2 = BooleanArray(2 * n - 1) // Record whether there is a queen on the anti-diagonal\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
    ### Backtracking: n queens ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n  # When all rows are placed, record the solution\n  if row == n\n    res << state.map { |row| row.dup }\n    return\n  end\n\n  # Traverse all columns\n  for col in 0...n\n    # Calculate the main diagonal and anti-diagonal corresponding to this cell\n    diag1 = row - col + n - 1\n    diag2 = row + col\n    # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n    if !cols[col] && !diags1[diag1] && !diags2[diag2]\n      # Attempt: place the queen in this cell\n      state[row][col] = \"Q\"\n      cols[col] = diags1[diag1] = diags2[diag2] = true\n      # Place the next row\n      backtrack(row + 1, n, state, res, cols, diags1, diags2)\n      # Backtrack: restore this cell to an empty cell\n      state[row][col] = \"#\"\n      cols[col] = diags1[diag1] = diags2[diag2] = false\n    end\n  end\nend\n\n### Solve n queens ###\ndef n_queens(n)\n  # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n  state = Array.new(n) { Array.new(n, \"#\") }\n  cols = Array.new(n, false) # Record whether there is a queen in the column\n  diags1 = Array.new(2 * n - 1, false) # Record whether there is a queen on the main diagonal\n  diags2 = Array.new(2 * n - 1, false) # Record whether there is a queen on the anti-diagonal\n  res = []\n  backtrack(0, n, state, res, cols, diags1, diags2)\n\n  res\nend\n

    Placing \\(n\\) queens row by row, considering the column constraint, from the first row to the last row there are \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\) choices, using \\(O(n!)\\) time. When recording a solution, it is necessary to copy the matrix state and add it to res, and the copy operation uses \\(O(n^2)\\) time. Therefore, the overall time complexity is \\(O(n! \\cdot n^2)\\). In practice, pruning based on diagonal constraints can also significantly reduce the search space, so the search efficiency is often better than the time complexity mentioned above.

    The array state uses \\(O(n^2)\\) space, and the arrays cols, diags1, and diags2 each use \\(O(n)\\) space. The maximum recursion depth is \\(n\\), using \\(O(n)\\) stack frame space. Therefore, the space complexity is \\(O(n^2)\\).

    ","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   Permutations Problem","text":"

    The permutations problem is a classic application of backtracking algorithms. It is defined as finding all possible arrangements of elements in a given collection (such as an array or string).

    Table 13-2 shows several example datasets, including input arrays and their corresponding permutations.

    Table 13-2   Permutations Examples

    Input Array All Permutations \\([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":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321-case-with-distinct-elements","level":2,"title":"13.2.1   Case with Distinct Elements","text":"

    Question

    Given an integer array with no duplicate elements, return all possible permutations.

    From the perspective of backtracking algorithms, we can imagine the process of generating permutations as the result of a series of choices. Suppose the input array is \\([1, 2, 3]\\). If we first choose \\(1\\), then choose \\(3\\), and finally choose \\(2\\), we obtain the permutation \\([1, 3, 2]\\). Backtracking means undoing a choice and then trying other choices.

    From the perspective of backtracking code, the candidate set choices consists of all elements in the input array, and the state state is the elements that have been chosen so far. Note that each element can only be chosen once, therefore all elements in state should be unique.

    As shown in Figure 13-5, we can unfold the search process into a recursion tree, where each node in the tree represents the current state state. Starting from the root node, after three rounds of choices, we reach a leaf node, and each leaf node corresponds to a permutation.

    Figure 13-5   Recursion tree of permutations

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1-pruning-duplicate-choices","level":3,"title":"1.   Pruning Duplicate Choices","text":"

    To ensure that each element is chosen only once, we consider introducing a boolean array selected, where selected[i] indicates whether choices[i] has been chosen. We implement the following pruning operation based on it.

    • After making a choice choices[i], we set selected[i] to \\(\\text{True}\\), indicating that it has been chosen.
    • When traversing the candidate list choices, we skip all nodes that have been chosen, which is pruning.

    As shown in Figure 13-6, suppose we choose \\(1\\) in the first round, \\(3\\) in the second round, and \\(2\\) in the third round. Then we need to prune the branch of element \\(1\\) in the second round and prune the branches of elements \\(1\\) and \\(3\\) in the third round.

    Figure 13-6   Pruning example of permutations

    Observing the above figure, we find that this pruning operation reduces the search space size from \\(O(n^n)\\) to \\(O(n!)\\).

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    After understanding the above information, we can fill in the blanks in the template code. To shorten the overall code, we do not implement each function in the template separately, but instead unfold them in the backtrack() function:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py
    def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Permutations I\"\"\"\n    # When the state length equals the number of elements, record the solution\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Traverse all choices\n    for i, choice in enumerate(choices):\n        # Pruning: do not allow repeated selection of elements\n        if not selected[i]:\n            # Attempt: make choice, update state\n            selected[i] = True\n            state.append(choice)\n            # Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            # Backtrack: undo choice, restore to previous state\n            selected[i] = False\n            state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n    \"\"\"Permutations I\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
    permutations_i.cpp
    /* Backtracking algorithm: Permutations I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push_back(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.add(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.Add(choice);\n            // Proceed to the next round of selection\n            Backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // When the state length equals the number of elements, record the solution\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Traverse all choices\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Pruning: do not allow repeated selection of elements\n        if !(*selected)[i] {\n            // Attempt: make choice, update state\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Proceed to the next round of selection\n            backtrackI(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // When the state length equals the number of elements, record the solution\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    for (i, choice) in choices.enumerated() {\n        // Pruning: do not allow repeated selection of elements\n        if !selected[i] {\n            // Attempt: make choice, update state\n            selected[i] = true\n            state.append(choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfunction backtrack(state, choices, selected, res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations I */\nfunction permutationsI(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
    permutations_i.ts
    /* Backtracking algorithm: Permutations I */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // When the state length equals the number of elements, record the solution\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Pruning: do not allow repeated selection of elements\n    if (!selected[i]) {\n      // Attempt: make choice, update state\n      selected[i] = true;\n      state.add(choice);\n      // Proceed to the next round of selection\n      backtrack(state, choices, selected, res);\n      // Backtrack: undo choice, restore to previous state\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // When the state length equals the number of elements, record the solution\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Traverse all choices\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if !selected[i] {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state.clone(), choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Permutations I */\nfn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new(); // State (subset)\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
    permutations_i.c
    /* Backtracking algorithm: Permutations I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // When the state length equals the number of elements, record the solution\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    // Traverse all choices\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Proceed to the next round of selection\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true\n            state.add(choice)\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Permutations 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
    ### Backtracking: permutations I ###\ndef backtrack(state, choices, selected, res)\n  # When the state length equals the number of elements, record the solution\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  choices.each_with_index do |choice, i|\n    # Pruning: do not allow repeated selection of elements\n    unless selected[i]\n      # Attempt: make choice, update state\n      selected[i] = true\n      state << choice\n      # Proceed to the next round of selection\n      backtrack(state, choices, selected, res)\n      # Backtrack: undo choice, restore to previous state\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Permutations I ###\ndef permutations_i(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322-case-with-duplicate-elements","level":2,"title":"13.2.2   Case with Duplicate Elements","text":"

    Question

    Given an integer array that may contain duplicate elements, return all unique permutations.

    Suppose the input array is \\([1, 1, 2]\\). To distinguish the two duplicate elements \\(1\\), we denote the second \\(1\\) as \\(\\hat{1}\\).

    As shown in Figure 13-7, half of the permutations generated by the above method are duplicates.

    Figure 13-7   Duplicate permutations

    So how do we remove duplicate permutations? The most direct approach is to use a hash set to directly deduplicate the permutation results. However, this is not elegant because the search branches that generate duplicate permutations are unnecessary and should be identified and pruned early, which can further improve algorithm efficiency.

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1-pruning-equal-elements","level":3,"title":"1.   Pruning Equal Elements","text":"

    Observe Figure 13-8. In the first round, choosing \\(1\\) or choosing \\(\\hat{1}\\) is equivalent. All permutations generated under these two choices are duplicates. Therefore, we should prune \\(\\hat{1}\\).

    Similarly, after choosing \\(2\\) in the first round, the \\(1\\) and \\(\\hat{1}\\) in the second round also produce duplicate branches, so the second round's \\(\\hat{1}\\) should also be pruned.

    Essentially, our goal is to ensure that multiple equal elements are chosen only once in a certain round of choices.

    Figure 13-8   Pruning duplicate permutations

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2-code-implementation_1","level":3,"title":"2.   Code Implementation","text":"

    Building on the code from the previous problem, we initialize a hash set duplicated in each round of choices to record which elements have already been tried in that round, and prune equal elements:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py
    def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Permutations II\"\"\"\n    # When the state length equals the number of elements, record the solution\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Traverse all choices\n    duplicated = set[int]()\n    for i, choice in enumerate(choices):\n        # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if not selected[i] and choice not in duplicated:\n            # Attempt: make choice, update state\n            duplicated.add(choice)  # Record the selected element value\n            selected[i] = True\n            state.append(choice)\n            # Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            # Backtrack: undo choice, restore to previous state\n            selected[i] = False\n            state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n    \"\"\"Permutations II\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
    permutations_ii.cpp
    /* Backtracking algorithm: Permutations II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    unordered_set<int> duplicated;\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n            // Attempt: make choice, update state\n            duplicated.emplace(choice); // Record the selected element value\n            selected[i] = true;\n            state.push_back(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Traverse all choices\n    Set<Integer> duplicated = new HashSet<Integer>();\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.add(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    HashSet<int> duplicated = [];\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.Contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.Add(choice); // Record the selected element value\n            selected[i] = true;\n            state.Add(choice);\n            // Proceed to the next round of selection\n            Backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // When the state length equals the number of elements, record the solution\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Traverse all choices\n    duplicated := make(map[int]struct{}, 0)\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if _, ok := duplicated[choice]; !ok && !(*selected)[i] {\n            // Attempt: make choice, update state\n            // Record the selected element value\n            duplicated[choice] = struct{}{}\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Proceed to the next round of selection\n            backtrackII(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // When the state length equals the number of elements, record the solution\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    var duplicated: Set<Int> = []\n    for (i, choice) in choices.enumerated() {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if !selected[i], !duplicated.contains(choice) {\n            // Attempt: make choice, update state\n            duplicated.insert(choice) // Record the selected element value\n            selected[i] = true\n            state.append(choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfunction backtrack(state, choices, selected, res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations II */\nfunction permutationsII(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
    permutations_ii.ts
    /* Backtracking algorithm: Permutations II */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // When the state length equals the number of elements, record the solution\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  Set<int> duplicated = {};\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n    if (!selected[i] && !duplicated.contains(choice)) {\n      // Attempt: make choice, update state\n      duplicated.add(choice); // Record the selected element value\n      selected[i] = true;\n      state.add(choice);\n      // Proceed to the next round of selection\n      backtrack(state, choices, selected, res);\n      // Backtrack: undo choice, restore to previous state\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // When the state length equals the number of elements, record the solution\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Traverse all choices\n    let mut duplicated = HashSet::<i32>::new();\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if !selected[i] && !duplicated.contains(&choice) {\n            // Attempt: make choice, update state\n            duplicated.insert(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state.clone(), choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // When the state length equals the number of elements, record the solution\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    // Traverse all choices\n    bool duplicated[MAX_SIZE] = {false};\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated[choice]) {\n            // Attempt: make choice, update state\n            duplicated[choice] = true; // Record the selected element value\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Proceed to the next round of selection\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    val duplicated = HashSet<Int>()\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice) // Record the selected element value\n            selected[i] = true\n            state.add(choice)\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Permutations 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
    ### Backtracking: permutations II ###\ndef backtrack(state, choices, selected, res)\n  # When the state length equals the number of elements, record the solution\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  duplicated = Set.new\n  choices.each_with_index do |choice, i|\n    # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n    if !selected[i] && !duplicated.include?(choice)\n      # Attempt: make choice, update state\n      duplicated.add(choice)\n      selected[i] = true\n      state << choice\n      # Proceed to the next round of selection\n      backtrack(state, choices, selected, res)\n      # Backtrack: undo choice, restore to previous state\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Permutations II ###\ndef permutations_ii(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n

    Assuming elements are pairwise distinct, there are \\(n!\\) (factorial) permutations of \\(n\\) elements. When recording results, we need to copy a list of length \\(n\\), using \\(O(n)\\) time. Therefore, the time complexity is \\(O(n! \\cdot n)\\).

    The maximum recursion depth is \\(n\\), using \\(O(n)\\) stack frame space. selected uses \\(O(n)\\) space. At most \\(n\\) duplicated sets exist simultaneously, using \\(O(n^2)\\) space. Therefore, the space complexity is \\(O(n^2)\\).

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3-comparison-of-two-pruning-methods","level":3,"title":"3.   Comparison of Two Pruning Methods","text":"

    Note that although both selected and duplicated are used for pruning, they have different objectives.

    • Pruning duplicate choices: There is only one selected throughout the entire search process. It records which elements are included in the current state, and its purpose is to prevent an element from appearing repeatedly in state.
    • Pruning equal elements: Each round of choices (each backtrack function call) contains a duplicated set. It records which elements have been chosen in this round's iteration (the for loop), and its purpose is to ensure that equal elements are chosen only once.

    Figure 13-9 shows the effective scope of the two pruning conditions. Note that each node in the tree represents a choice, and the nodes on the path from the root to a leaf node form a permutation.

    Figure 13-9   Effective scope of two pruning conditions

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   Subset-Sum Problem","text":"","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331-without-duplicate-elements","level":2,"title":"13.3.1   Without Duplicate Elements","text":"

    Question

    Given a positive integer array nums and a target positive integer target, find all possible combinations where the sum of elements in the combination equals target. The given array has no duplicate elements, and each element can be selected multiple times. Return these combinations in list form, where the list should not contain duplicate combinations.

    For example, given the set \\(\\{3, 4, 5\\}\\) and target integer \\(9\\), the solutions are \\(\\{3, 3, 3\\}, \\{4, 5\\}\\). Note the following two points:

    • Elements in the input set can be selected repeatedly without limit.
    • Subsets do not distinguish element order; for example, \\(\\{4, 5\\}\\) and \\(\\{5, 4\\}\\) are the same subset.
    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1-using-the-permutation-solution-as-a-reference","level":3,"title":"1.   Using the Permutation Solution as a Reference","text":"

    Similar to the permutation problem, we can view the process of generating subsets as the result of a series of choices and update the running sum during the selection process. When the sum equals target, we record the subset in the result list.

    Unlike the permutation problem, elements in this problem can be selected any number of times, so we do not need to use a selected boolean list to track whether an element has already been selected. With a few small changes to the permutation code, we obtain an initial solution:

    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    \"\"\"Backtracking algorithm: Subset sum I\"\"\"\n    # When the subset sum equals target, record the solution\n    if total == target:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    for i in range(len(choices)):\n        # Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target:\n            continue\n        # Attempt: make choice, update element sum total\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum I (including duplicate subsets)\"\"\"\n    state = []  # State (subset)\n    total = 0  # Subset sum\n    res = []  # Result list (subset list)\n    backtrack(state, target, total, nums, res)\n    return res\n
    subset_sum_i_naive.cpp
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    for (size_t i = 0; i < choices.size(); i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nvector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {\n    vector<int> state;       // State (subset)\n    int total = 0;           // Subset sum\n    vector<vector<int>> res; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.java
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    int total = 0; // Subset sum\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.cs
    /* Backtracking algorithm: Subset sum I */\nvoid Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.Length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<int>> SubsetSumINaive(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    int total = 0; // Subset sum\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.go
    /* Backtracking algorithm: Subset sum I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == total {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    for i := 0; i < len(*choices); i++ {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total+(*choices)[i] > target {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    total := 0              // Subset sum\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumINaive(total, target, &state, &nums, &res)\n    return res\n}\n
    subset_sum_i_naive.swift
    /* Backtracking algorithm: Subset sum I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if total == target {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    for i in choices.indices {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunc subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let total = 0 // Subset sum\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, total: total, choices: nums, res: &res)\n    return res\n}\n
    subset_sum_i_naive.js
    /* Backtracking algorithm: Subset sum I */\nfunction backtrack(state, target, total, choices, res) {\n    // When the subset sum equals target, record the solution\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    for (let i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunction subsetSumINaive(nums, target) {\n    const state = []; // State (subset)\n    const total = 0; // Subset sum\n    const res = []; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.ts
    /* Backtracking algorithm: Subset sum I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    total: number,\n    choices: number[],\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    for (let i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    const total = 0; // Subset sum\n    const res = []; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.dart
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  int total,\n  List<int> choices,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (total == target) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  for (int i = 0; i < choices.length; i++) {\n    // Pruning: if the subset sum exceeds target, skip this choice\n    if (total + choices[i] > target) {\n      continue;\n    }\n    // Attempt: make choice, update element sum total\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target, total + choices[i], choices, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<int>> subsetSumINaive(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  int total = 0; // Sum of elements\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, total, nums, res);\n  return res;\n}\n
    subset_sum_i_naive.rs
    /* Backtracking algorithm: Subset sum 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    // When the subset sum equals target, record the solution\n    if total == target {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    for i in 0..choices.len() {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    let total = 0; // Subset sum\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, total, nums, &mut res);\n    res\n}\n
    subset_sum_i_naive.c
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n    // When the subset sum equals target, record the solution\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    // Traverse all choices\n    for (int i = 0; i < choicesSize; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state[stateSize++] = choices[i];\n        // Proceed to the next round of selection\n        backtrack(target, total + choices[i], choices, choicesSize);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n    resSize = 0; // Initialize solution count to 0\n    backtrack(target, 0, nums, numsSize);\n}\n
    subset_sum_i_naive.kt
    /* Backtracking algorithm: Subset sum I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    total: Int,\n    choices: IntArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    for (i in choices.indices) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    val total = 0 // Subset sum\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, total, nums, res)\n    return res\n}\n
    subset_sum_i_naive.rb
    ### Backtracking: subset sum I ###\ndef backtrack(state, target, total, choices, res)\n  # When the subset sum equals target, record the solution\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  for i in 0...choices.length\n    # Pruning: if the subset sum exceeds target, skip this choice\n    next if total + choices[i] > target\n    # Attempt: make choice, update element sum total\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target, total + choices[i], choices, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum I (with duplicate subsets) ###\ndef subset_sum_i_naive(nums, target)\n  state = [] # State (subset)\n  total = 0 # Subset sum\n  res = [] # Result list (subset list)\n  backtrack(state, target, total, nums, res)\n  res\nend\n

    Running the above code on array \\([3, 4, 5]\\) with target value \\(9\\) produces \\([3, 3, 3], [4, 5], [5, 4]\\). Although we successfully found all subsets that sum to \\(9\\), there are duplicate subsets \\([4, 5]\\) and \\([5, 4]\\).

    This is because the search process distinguishes the order of selections, but subsets do not distinguish selection order. As shown in Figure 13-10, selecting 4 first and then 5 versus selecting 5 first and then 4 are different branches, but they correspond to the same subset.

    Figure 13-10   Subset search and boundary pruning

    To eliminate duplicate subsets, one straightforward idea is to deduplicate the result list. However, this approach is very inefficient for two reasons:

    • When there are many array elements, especially when target is large, the search process generates many duplicate subsets.
    • Comparing subsets (arrays) is very time-consuming, requiring sorting the arrays first, then comparing each element in them.
    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2-pruning-duplicate-subsets","level":3,"title":"2.   Pruning Duplicate Subsets","text":"

    We consider deduplication through pruning during the search process. Observing Figure 13-11, duplicate subsets occur when array elements are selected in different orders, as in the following cases:

    1. When the first and second rounds select \\(3\\) and \\(4\\) respectively, all subsets containing these two elements are generated, denoted as \\([3, 4, \\dots]\\).
    2. Afterward, when the first round selects \\(4\\), the second round should skip \\(3\\), because the subset \\([4, 3, \\dots]\\) generated by this choice is an exact duplicate of the subset generated in step 1.

    In the search process, each level's choices are tried from left to right, so the rightmost branches are pruned more.

    1. The first two rounds select \\(3\\) and \\(5\\), generating subset \\([3, 5, \\dots]\\).
    2. The first two rounds select \\(4\\) and \\(5\\), generating subset \\([4, 5, \\dots]\\).
    3. If the first round selects \\(5\\), the second round should skip \\(3\\) and \\(4\\), because subsets \\([5, 3, \\dots]\\) and \\([5, 4, \\dots]\\) are exact duplicates of the subsets described in steps 1. and 2.

    Figure 13-11   Different selection orders leading to duplicate subsets

    In summary, given an input array \\([x_1, x_2, \\dots, x_n]\\), let the selection sequence in the search process be \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\). This selection sequence must satisfy \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\); any selection sequence that does not satisfy this condition will cause duplicates and should be pruned.

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

    To implement this pruning, we initialize a variable start to indicate the starting point of traversal. After making choice \\(x_{i}\\), set the next round to start traversal from index \\(i\\). This ensures that the selection sequence satisfies \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\), guaranteeing subset uniqueness.

    In addition, we have made the following two optimizations to the code:

    • Before starting the search, first sort the array nums. When traversing all choices, end the loop immediately when the subset sum exceeds target, because subsequent elements are larger, and their subset sums must exceed target.
    • Omit the element sum variable total and use subtraction on target to track the sum of elements. Record the solution when target equals \\(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    \"\"\"Backtracking algorithm: Subset sum I\"\"\"\n    # When the subset sum equals target, record the solution\n    if target == 0:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    # Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in range(start, len(choices)):\n        # Pruning 1: if the subset sum exceeds target, end the loop directly\n        # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0:\n            break\n        # Attempt: make choice, update target, start\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum I\"\"\"\n    state = []  # State (subset)\n    nums.sort()  # Sort nums\n    start = 0  # Start point for traversal\n    res = []  # Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n
    subset_sum_i.cpp
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.size(); i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum I */\nvector<vector<int>> subsetSumI(vector<int> &nums, int target) {\n    vector<int> state;              // State (subset)\n    sort(nums.begin(), nums.end()); // Sort nums\n    int start = 0;                  // Start point for traversal\n    vector<vector<int>> res;        // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.java
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum I */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    Arrays.sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.cs
    /* Backtracking algorithm: Subset sum I */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.Length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum I */\nList<List<int>> SubsetSumI(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    Array.Sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.go
    /* Backtracking algorithm: Subset sum I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i := start; i < len(*choices); i++ {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Attempt: make choice, update target, start\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum I */\nfunc subsetSumI(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    sort.Ints(nums)         // Sort nums\n    start := 0              // Start point for traversal\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumI(start, target, &state, &nums, &res)\n    return res\n}\n
    subset_sum_i.swift
    /* Backtracking algorithm: Subset sum I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in choices.indices.dropFirst(start) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Attempt: make choice, update target, start\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum I */\nfunc subsetSumI(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let nums = nums.sorted() // Sort nums\n    let start = 0 // Start point for traversal\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
    subset_sum_i.js
    /* Backtracking algorithm: Subset sum I */\nfunction backtrack(state, target, choices, start, res) {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfunction subsetSumI(nums, target) {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.ts
    /* Backtracking algorithm: Subset sum I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.dart
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  // Pruning 2: start traversing from start to avoid generating duplicate subsets\n  for (int i = start; i < choices.length; i++) {\n    // Pruning 1: if the subset sum exceeds target, end the loop directly\n    // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Attempt: make choice, update target, start\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum I */\nList<List<int>> subsetSumI(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  nums.sort(); // Sort nums\n  int start = 0; // Start point for traversal\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
    subset_sum_i.rs
    /* Backtracking algorithm: Subset sum 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    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in start..choices.len() {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    nums.sort(); // Sort nums\n    let start = 0; // Start point for traversal\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
    subset_sum_i.c
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // When the subset sum equals target, record the solution\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    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choicesSize; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Proceed to the next round of selection\n        backtrack(target - choices[i], choices, choicesSize, i);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum I */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n    qsort(nums, numsSize, sizeof(int), cmp); // Sort nums\n    int start = 0;                           // Start point for traversal\n    backtrack(target, nums, numsSize, start);\n}\n
    subset_sum_i.kt
    /* Backtracking algorithm: Subset sum I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (i in start..<choices.size) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum I */\nfun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    nums.sort() // Sort nums\n    val start = 0 // Start point for traversal\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
    subset_sum_i.rb
    ### Backtracking: subset sum I ###\ndef backtrack(state, target, choices, start, res)\n  # When the subset sum equals target, record the solution\n  if target.zero?\n    res << state.dup\n    return\n  end\n  # Traverse all choices\n  # Pruning 2: start traversing from start to avoid generating duplicate subsets\n  for i in start...choices.length\n    # Pruning 1: if the subset sum exceeds target, end the loop directly\n    # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    break if target - choices[i] < 0\n    # Attempt: make choice, update target, start\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum I ###\ndef subset_sum_i(nums, target)\n  state = [] # State (subset)\n  nums.sort! # Sort nums\n  start = 0 # Start point for traversal\n  res = [] # Result list (subset list)\n  backtrack(state, target, nums, start, res)\n  res\nend\n

    Figure 13-12 shows the complete backtracking process produced by running the above code on array \\([3, 4, 5]\\) with target value \\(9\\).

    Figure 13-12   Subset-sum I backtracking process

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332-with-duplicate-elements-in-array","level":2,"title":"13.3.2   With Duplicate Elements in Array","text":"

    Question

    Given a positive integer array nums and a target positive integer target, find all possible combinations where the sum of elements in the combination equals target. The given array may contain duplicate elements, and each element can be selected at most once. Return these combinations in list form, where the list should not contain duplicate combinations.

    Compared to the previous problem, the input array in this problem may contain duplicate elements, which introduces a new issue. For example, given array \\([4, \\hat{4}, 5]\\) and target value \\(9\\), the output of the existing code is \\([4, 5], [\\hat{4}, 5]\\), which contains duplicate subsets.

    The reason for this duplication is that equal elements are selected multiple times in a certain round. In Figure 13-13, the first round has three choices, two of which are \\(4\\), creating two duplicate search branches that output duplicate subsets. Similarly, the two \\(4\\)'s in the second round also produce duplicate subsets.

    Figure 13-13   Duplicate subsets caused by equal elements

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1-pruning-equal-elements","level":3,"title":"1.   Pruning Equal Elements","text":"

    To solve this problem, we need to limit equal elements to be selected only once in each round. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a given round of selection, if the current element equals the element to its left, then the same value has already been chosen in this round, so we skip the current element directly.

    At the same time, this problem specifies that each array element can only be selected once. Fortunately, we can also use the variable start to satisfy this constraint: after making choice \\(x_{i}\\), set the next round to start traversal from index \\(i + 1\\) onwards. This both eliminates duplicate subsets and avoids selecting elements multiple times.

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","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    \"\"\"Backtracking algorithm: Subset sum II\"\"\"\n    # When the subset sum equals target, record the solution\n    if target == 0:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    # Pruning 2: start traversing from start to avoid generating duplicate subsets\n    # Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in range(start, len(choices)):\n        # Pruning 1: if the subset sum exceeds target, end the loop directly\n        # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0:\n            break\n        # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start and choices[i] == choices[i - 1]:\n            continue\n        # Attempt: make choice, update target, start\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum II\"\"\"\n    state = []  # State (subset)\n    nums.sort()  # Sort nums\n    start = 0  # Start point for traversal\n    res = []  # Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n
    subset_sum_ii.cpp
    /* Backtracking algorithm: Subset sum II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.size(); i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum II */\nvector<vector<int>> subsetSumII(vector<int> &nums, int target) {\n    vector<int> state;              // State (subset)\n    sort(nums.begin(), nums.end()); // Sort nums\n    int start = 0;                  // Start point for traversal\n    vector<vector<int>> res;        // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.java
    /* Backtracking algorithm: Subset sum II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum II */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    Arrays.sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.cs
    /* Backtracking algorithm: Subset sum II */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.Length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum II */\nList<List<int>> SubsetSumII(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    Array.Sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.go
    /* Backtracking algorithm: Subset sum II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i := start; i < len(*choices); i++ {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start && (*choices)[i] == (*choices)[i-1] {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum II */\nfunc subsetSumII(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    sort.Ints(nums)         // Sort nums\n    start := 0              // Start point for traversal\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumII(start, target, &state, &nums, &res)\n    return res\n}\n
    subset_sum_ii.swift
    /* Backtracking algorithm: Subset sum II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in choices.indices.dropFirst(start) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start, choices[i] == choices[i - 1] {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum II */\nfunc subsetSumII(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let nums = nums.sorted() // Sort nums\n    let start = 0 // Start point for traversal\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
    subset_sum_ii.js
    /* Backtracking algorithm: Subset sum II */\nfunction backtrack(state, target, choices, start, res) {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfunction subsetSumII(nums, target) {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.ts
    /* Backtracking algorithm: Subset sum II */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.dart
    /* Backtracking algorithm: Subset sum II */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  // Pruning 2: start traversing from start to avoid generating duplicate subsets\n  // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n  for (int i = start; i < choices.length; i++) {\n    // Pruning 1: if the subset sum exceeds target, end the loop directly\n    // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n    if (i > start && choices[i] == choices[i - 1]) {\n      continue;\n    }\n    // Attempt: make choice, update target, start\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i + 1, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum II */\nList<List<int>> subsetSumII(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  nums.sort(); // Sort nums\n  int start = 0; // Start point for traversal\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
    subset_sum_ii.rs
    /* Backtracking algorithm: Subset sum 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    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in start..choices.len() {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start && choices[i] == choices[i - 1] {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    nums.sort(); // Sort nums\n    let start = 0; // Start point for traversal\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
    subset_sum_ii.c
    /* Backtracking algorithm: Subset sum II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // When the subset sum equals target, record the solution\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    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choicesSize; i++) {\n        // Pruning 1: Skip if subset sum exceeds target\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Proceed to the next round of selection\n        backtrack(target - choices[i], choices, choicesSize, i + 1);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum II */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n    // Sort nums\n    qsort(nums, numsSize, sizeof(int), cmp);\n    // Start backtracking\n    backtrack(target, nums, numsSize, 0);\n}\n
    subset_sum_ii.kt
    /* Backtracking algorithm: Subset sum II */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (i in start..<choices.size) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum II */\nfun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    nums.sort() // Sort nums\n    val start = 0 // Start point for traversal\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
    subset_sum_ii.rb
    ### Backtracking: subset sum II ###\ndef backtrack(state, target, choices, start, res)\n  # When the subset sum equals target, record the solution\n  if target.zero?\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  # Pruning 2: start traversing from start to avoid generating duplicate subsets\n  # Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n  for i in start...choices.length\n    # Pruning 1: if the subset sum exceeds target, end the loop directly\n    # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    break if target - choices[i] < 0\n    # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n    next if i > start && choices[i] == choices[i - 1]\n    # Attempt: make choice, update target, start\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i + 1, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum II ###\ndef subset_sum_ii(nums, target)\n  state = [] # State (subset)\n  nums.sort! # Sort nums\n  start = 0 # Start point for traversal\n  res = [] # Result list (subset list)\n  backtrack(state, target, nums, start, res)\n  res\nend\n

    Figure 13-14 shows the backtracking process for array \\([4, 4, 5]\\) with target value \\(9\\), which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works.

    Figure 13-14   Subset-sum II backtracking process

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   Summary","text":"","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_backtracking/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • The backtracking algorithm is fundamentally an exhaustive search method. It finds solutions that meet specified conditions by performing a depth-first traversal of the solution space. During the search process, when a solution satisfying the conditions is found, it is recorded. The search ends either after finding all solutions or when the traversal is complete.
    • The backtracking algorithm search process consists of two parts: attempting and backtracking. It tries various choices through depth-first search. When encountering situations that violate constraints, it reverts the previous choice, returns to the previous state, and continues exploring other options. Attempting and backtracking are operations in opposite directions.
    • Backtracking problems typically contain multiple constraints, which can be utilized to implement pruning operations. Pruning can terminate unnecessary search branches early, significantly improving search efficiency.
    • The backtracking algorithm is primarily used to solve search problems and constraint satisfaction problems. While combinatorial optimization problems can be solved with backtracking, there are often more efficient or better-performing solutions available.
    • The permutation problem aims to find all possible permutations of elements in a given set. We use an array to record whether each element has been selected, thereby pruning search branches that attempt to select the same element repeatedly, ensuring each element is selected exactly once.
    • In the permutation problem, if the set contains duplicate elements, the final result will contain duplicate permutations. We need to impose a constraint so that equal elements can only be selected once per round, which is typically achieved using a hash set.
    • The subset-sum problem aims to find all subsets of a given set that sum to a target value. Since the set is unordered but the search process outputs results in all orders, duplicate subsets are generated. We sort the data before backtracking and use a variable to indicate the starting point of each round's traversal, thereby pruning search branches that generate duplicate subsets.
    • For the subset-sum problem, equal elements in the array produce duplicate subsets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round.
    • The \\(n\\) queens problem aims to find placements of \\(n\\) queens on an \\(n \\times n\\) chessboard such that no two queens can attack each other. The constraints of this problem include row constraints, column constraints, and main and anti-diagonal constraints. To satisfy row constraints, we adopt a row-by-row placement strategy, ensuring exactly one queen is placed in each row.
    • The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether each column has a queen, thereby indicating whether a selected cell is valid. For diagonal constraints, we use two arrays to separately record whether queens exist on each main or anti-diagonal. The challenge lies in finding the row-column index pattern that characterizes cells on the same main (anti-)diagonal.
    ","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: How can we understand the relationship between backtracking and recursion?

    Overall, backtracking is an algorithmic strategy, while recursion is better viewed as a tool.

    • Backtracking is typically implemented with recursion. However, backtracking is only one application of recursion, specifically its use in search problems.
    • The structure of recursion reflects a problem-solving paradigm based on decomposing a problem into subproblems, and it is commonly used in divide-and-conquer, backtracking, and dynamic programming (memoized recursion).
    ","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"Chapter 2.   Complexity Analysis","text":"

    Abstract

    Complexity analysis is like a space-time guide in the vast universe of algorithms.

    It leads us to explore deeply within the two dimensions of time and space, seeking more elegant solutions.

    ","path":["Chapter 2. Complexity Analysis","Chapter 2.   Complexity Analysis"],"tags":[]},{"location":"chapter_computational_complexity/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 2.1   Algorithm Efficiency Evaluation
    • 2.2   Iteration and Recursion
    • 2.3   Time Complexity
    • 2.4   Space Complexity
    • 2.5   Summary
    ","path":["Chapter 2. Complexity Analysis","Chapter 2.   Complexity Analysis"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   Iteration and Recursion","text":"

    In algorithms, repeatedly executing a task is very common and closely related to complexity analysis. Therefore, before introducing time complexity and space complexity, let's first understand how to implement repeated task execution in programs, namely the two basic program control structures: iteration and recursion.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221-iteration","level":2,"title":"2.2.1   Iteration","text":"

    Iteration is a control structure for repeatedly executing a task. In iteration, a program repeatedly executes a segment of code under certain conditions until those conditions are no longer satisfied.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for-loop","level":3,"title":"1.   For Loop","text":"

    The for loop is one of the most common forms of iteration, suitable for use when the number of iterations is known in advance.

    The following function implements the summation \\(1 + 2 + \\dots + n\\) using a for loop, with the result stored in the variable res. Note that in Python, range(a, b) corresponds to a \"left-closed, right-open\" interval, with the traversal range being \\(a, a + 1, \\dots, b-1\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def for_loop(n: int) -> int:\n    \"\"\"for loop\"\"\"\n    res = 0\n    # Sum 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        res += i\n    return res\n
    iteration.cpp
    /* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 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 loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 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 loop */\nint ForLoop(int n) {\n    int res = 0;\n    // Sum 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 loop */\nfunc forLoop(n int) int {\n    res := 0\n    // Sum 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        res += i\n    }\n    return res\n}\n
    iteration.swift
    /* for loop */\nfunc forLoop(n: Int) -> Int {\n    var res = 0\n    // Sum 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        res += i\n    }\n    return res\n}\n
    iteration.js
    /* for loop */\nfunction forLoop(n) {\n    let res = 0;\n    // Sum 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 loop */\nfunction forLoop(n: number): number {\n    let res = 0;\n    // Sum 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 loop */\nint forLoop(int n) {\n  int res = 0;\n  // Sum 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 loop */\nfn for_loop(n: i32) -> i32 {\n    let mut res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for i in 1..=n {\n        res += i;\n    }\n    res\n}\n
    iteration.c
    /* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 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 loop */\nfun forLoop(n: Int): Int {\n    var res = 0\n    // Sum 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        res += i\n    }\n    return res\n}\n
    iteration.rb
    ### for loop ###\ndef for_loop(n)\n  res = 0\n\n  # Sum 1, 2, ..., n-1, n\n  for i in 1..n\n    res += i\n  end\n\n  res\nend\n

    Figure 2-1 shows the flowchart of this summation function.

    Figure 2-1   Flowchart of the summation function

    The number of operations in this summation function is proportional to the input data size \\(n\\), or has a \"linear relationship\". In fact, time complexity describes precisely this \"linear relationship\". Related content will be introduced in detail in the next section.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while-loop","level":3,"title":"2.   While Loop","text":"

    Similar to the for loop, the while loop is also a method for implementing iteration. In a while loop, the program first checks the condition in each round; if the condition is true, it continues execution, otherwise it ends the loop.

    Below we use a while loop to implement the summation \\(1 + 2 + \\dots + n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def while_loop(n: int) -> int:\n    \"\"\"while loop\"\"\"\n    res = 0\n    i = 1  # Initialize condition variable\n    # Sum 1, 2, ..., n-1, n\n    while i <= n:\n        res += i\n        i += 1  # Update condition variable\n    return res\n
    iteration.cpp
    /* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.java
    /* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.cs
    /* while loop */\nint WhileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i += 1; // Update condition variable\n    }\n    return res;\n}\n
    iteration.go
    /* while loop */\nfunc whileLoop(n int) int {\n    res := 0\n    // Initialize condition variable\n    i := 1\n    // Sum 1, 2, ..., n-1, n\n    for i <= n {\n        res += i\n        // Update condition variable\n        i++\n    }\n    return res\n}\n
    iteration.swift
    /* while loop */\nfunc whileLoop(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while i <= n {\n        res += i\n        i += 1 // Update condition variable\n    }\n    return res\n}\n
    iteration.js
    /* while loop */\nfunction whileLoop(n) {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.ts
    /* while loop */\nfunction whileLoop(n: number): number {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.dart
    /* while loop */\nint whileLoop(int n) {\n  int res = 0;\n  int i = 1; // Initialize condition variable\n  // Sum 1, 2, ..., n-1, n\n  while (i <= n) {\n    res += i;\n    i++; // Update condition variable\n  }\n  return res;\n}\n
    iteration.rs
    /* while loop */\nfn while_loop(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Initialize condition variable\n\n    // Sum 1, 2, ..., n-1, n\n    while i <= n {\n        res += i;\n        i += 1; // Update condition variable\n    }\n    res\n}\n
    iteration.c
    /* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.kt
    /* while loop */\nfun whileLoop(n: Int): Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i\n        i++ // Update condition variable\n    }\n    return res\n}\n
    iteration.rb
    ### while loop ###\ndef while_loop(n)\n  res = 0\n  i = 1 # Initialize condition variable\n\n  # Sum 1, 2, ..., n-1, n\n  while i <= n\n    res += i\n    i += 1 # Update condition variable\n  end\n\n  res\nend\n

    The while loop has greater flexibility than the for loop. In a while loop, we can freely design the initialization and update steps of the condition variable.

    For example, in the following code, the condition variable \\(i\\) is updated twice per round, which is not convenient to implement using a for loop:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def while_loop_ii(n: int) -> int:\n    \"\"\"while loop (two updates)\"\"\"\n    res = 0\n    i = 1  # Initialize condition variable\n    # Sum 1, 4, 10, ...\n    while i <= n:\n        res += i\n        # Update condition variable\n        i += 1\n        i *= 2\n    return res\n
    iteration.cpp
    /* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.java
    /* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.cs
    /* while loop (two updates) */\nint WhileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i += 1; \n        i *= 2;\n    }\n    return res;\n}\n
    iteration.go
    /* while loop (two updates) */\nfunc whileLoopII(n int) int {\n    res := 0\n    // Initialize condition variable\n    i := 1\n    // Sum 1, 4, 10, ...\n    for i <= n {\n        res += i\n        // Update condition variable\n        i++\n        i *= 2\n    }\n    return res\n}\n
    iteration.swift
    /* while loop (two updates) */\nfunc whileLoopII(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while i <= n {\n        res += i\n        // Update condition variable\n        i += 1\n        i *= 2\n    }\n    return res\n}\n
    iteration.js
    /* while loop (two updates) */\nfunction whileLoopII(n) {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.ts
    /* while loop (two updates) */\nfunction whileLoopII(n: number): number {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.dart
    /* while loop (two updates) */\nint whileLoopII(int n) {\n  int res = 0;\n  int i = 1; // Initialize condition variable\n  // Sum 1, 4, 10, ...\n  while (i <= n) {\n    res += i;\n    // Update condition variable\n    i++;\n    i *= 2;\n  }\n  return res;\n}\n
    iteration.rs
    /* while loop (two updates) */\nfn while_loop_ii(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Initialize condition variable\n\n    // Sum 1, 4, 10, ...\n    while i <= n {\n        res += i;\n        // Update condition variable\n        i += 1;\n        i *= 2;\n    }\n    res\n}\n
    iteration.c
    /* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.kt
    /* while loop (two updates) */\nfun whileLoopII(n: Int): Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i\n        // Update condition variable\n        i++\n        i *= 2\n    }\n    return res\n}\n
    iteration.rb
    ### while loop (two updates) ###\ndef while_loop_ii(n)\n  res = 0\n  i = 1 # Initialize condition variable\n\n  # Sum 1, 4, 10, ...\n  while i <= n\n    res += i\n    # Update condition variable\n    i += 1\n    i *= 2\n  end\n\n  res\nend\n

    Overall, for loops have more compact code, while while loops are more flexible; both can implement iterative structures. The choice of which to use should be determined based on the requirements of the specific problem.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3-nested-loops","level":3,"title":"3.   Nested Loops","text":"

    We can nest one loop structure inside another. Below is an example using for loops:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def nested_for_loop(n: int) -> str:\n    \"\"\"Nested for loop\"\"\"\n    res = \"\"\n    # Loop i = 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        # Loop 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
    /* Nested for loop */\nstring nestedForLoop(int n) {\n    ostringstream res;\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        // Loop 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
    /* Nested for loop */\nString nestedForLoop(int n) {\n    StringBuilder res = new StringBuilder();\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nstring NestedForLoop(int n) {\n    StringBuilder res = new();\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nfunc nestedForLoop(n int) string {\n    res := \"\"\n    // Loop i = 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= n; j++ {\n            // Loop j = 1, 2, ..., n-1, n\n            res += fmt.Sprintf(\"(%d, %d), \", i, j)\n        }\n    }\n    return res\n}\n
    iteration.swift
    /* Nested for loop */\nfunc nestedForLoop(n: Int) -> String {\n    var res = \"\"\n    // Loop i = 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        // Loop 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
    /* Nested for loop */\nfunction nestedForLoop(n) {\n    let res = '';\n    // Loop i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nfunction nestedForLoop(n: number): string {\n    let res = '';\n    // Loop i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nString nestedForLoop(int n) {\n  String res = \"\";\n  // Loop i = 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    // Loop 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
    /* Nested for loop */\nfn nested_for_loop(n: i32) -> String {\n    let mut res = vec![];\n    // Loop i = 1, 2, ..., n-1, n\n    for i in 1..=n {\n        // Loop 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
    /* Nested for loop */\nchar *nestedForLoop(int n) {\n    // n * n is the number of points, \"(i, j), \" string max length is 6+10*2, plus extra space for null character \\0\n    int size = n * n * 26 + 1;\n    char *res = malloc(size * sizeof(char));\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nfun nestedForLoop(n: Int): String {\n    val res = StringBuilder()\n    // Loop i = 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        // Loop 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
    ### Nested for loop ###\ndef nested_for_loop(n)\n  res = \"\"\n\n  # Loop i = 1, 2, ..., n-1, n\n  for i in 1..n\n    # Loop 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

    Figure 2-2 shows the flowchart of this nested loop.

    Figure 2-2   Flowchart of nested loops

    In this case, the number of operations of the function is proportional to \\(n^2\\), or the algorithm's running time has a \"quadratic relationship\" with the input data size \\(n\\).

    We can continue adding nested loops, where each additional level of nesting can be viewed as an increase in dimensionality, raising the time complexity to a \"cubic relationship\", a \"quartic relationship\", and so on.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222-recursion","level":2,"title":"2.2.2   Recursion","text":"

    Recursion is an algorithmic strategy that solves problems by having a function call itself. It mainly consists of two phases.

    1. Descend: The program continuously calls itself deeper, usually passing in smaller or more simplified parameters, until reaching a \"termination condition\".
    2. Ascend: After triggering the \"termination condition\", the program returns layer by layer from the deepest recursive function, aggregating the result of each layer.

    From an implementation perspective, recursive code mainly consists of three elements.

    1. Termination condition: Used to determine when to switch from \"descending\" to \"ascending\".
    2. Recursive call: Corresponds to \"descending\", where the function calls itself, usually with smaller or more simplified parameters.
    3. Return result: Corresponds to \"ascending\", returning the result of the current recursion level to the previous layer.

    Observe the following code. We only need to call the function recur(n) to complete the calculation of \\(1 + 2 + \\dots + n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
    def recur(n: int) -> int:\n    \"\"\"Recursion\"\"\"\n    # Termination condition\n    if n == 1:\n        return 1\n    # Recurse: recursive call\n    res = recur(n - 1)\n    # Return: return result\n    return n + res\n
    recursion.cpp
    /* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.java
    /* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.cs
    /* Recursion */\nint Recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = Recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.go
    /* Recursion */\nfunc recur(n int) int {\n    // Termination condition\n    if n == 1 {\n        return 1\n    }\n    // Recurse: recursive call\n    res := recur(n - 1)\n    // Return: return result\n    return n + res\n}\n
    recursion.swift
    /* Recursion */\nfunc recur(n: Int) -> Int {\n    // Termination condition\n    if n == 1 {\n        return 1\n    }\n    // Recurse: recursive call\n    let res = recur(n: n - 1)\n    // Return: return result\n    return n + res\n}\n
    recursion.js
    /* Recursion */\nfunction recur(n) {\n    // Termination condition\n    if (n === 1) return 1;\n    // Recurse: recursive call\n    const res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.ts
    /* Recursion */\nfunction recur(n: number): number {\n    // Termination condition\n    if (n === 1) return 1;\n    // Recurse: recursive call\n    const res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.dart
    /* Recursion */\nint recur(int n) {\n  // Termination condition\n  if (n == 1) return 1;\n  // Recurse: recursive call\n  int res = recur(n - 1);\n  // Return: return result\n  return n + res;\n}\n
    recursion.rs
    /* Recursion */\nfn recur(n: i32) -> i32 {\n    // Termination condition\n    if n == 1 {\n        return 1;\n    }\n    // Recurse: recursive call\n    let res = recur(n - 1);\n    // Return: return result\n    n + res\n}\n
    recursion.c
    /* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.kt
    /* Recursion */\nfun recur(n: Int): Int {\n    // Termination condition\n    if (n == 1)\n        return 1\n    // Descend: recursive call\n    val res = recur(n - 1)\n    // Return: return result\n    return n + res\n}\n
    recursion.rb
    ### Recursion ###\ndef recur(n)\n  # Termination condition\n  return 1 if n == 1\n  # Recurse: recursive call\n  res = recur(n - 1)\n  # Return: return result\n  n + res\nend\n

    Figure 2-3 shows the recursive process of this function.

    Figure 2-3   Recursive process of the summation function

    Although from a computational perspective, iteration and recursion can achieve the same results, they represent two completely different paradigms for thinking about and solving problems.

    • Iteration: Solves problems \"bottom-up\". Starting from the most basic steps, these steps are then repeatedly executed or accumulated until the task is complete.
    • Recursion: Solves problems \"top-down\". The original problem is decomposed into smaller subproblems that have the same form as the original problem. These subproblems continue to be decomposed into even smaller subproblems until reaching the base case (where the solution is known).

    Taking the above summation function as an example, let the problem be \\(f(n) = 1 + 2 + \\dots + n\\).

    • Iteration: Simulates the summation process in a loop, traversing from \\(1\\) to \\(n\\), performing the summation operation in each round to obtain \\(f(n)\\).
    • Recursion: Decomposes the problem into the subproblem \\(f(n) = n + f(n-1)\\), continuously decomposing (recursively) until terminating at the base case \\(f(1) = 1\\).
    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-call-stack","level":3,"title":"1.   Call Stack","text":"

    Each time a recursive function calls itself, the system allocates memory for the newly invoked function to store local variables, call addresses, and other information. This leads to two consequences.

    • The function's context data is stored in a memory area called \"stack frame space\", which is not released until the function returns. Therefore, recursion usually consumes more memory space than iteration.
    • Recursive function calls incur additional overhead. Therefore, recursion is usually less time-efficient than loops.

    As shown in Figure 2-4, before the termination condition is triggered, there are \\(n\\) unreturned recursive functions existing simultaneously, with a recursion depth of \\(n\\).

    Figure 2-4   Recursion call depth

    In practice, the recursion depth allowed by programming languages is usually limited, and excessively deep recursion may lead to stack overflow errors.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-tail-recursion","level":3,"title":"2.   Tail Recursion","text":"

    Interestingly, if a function makes the recursive call as the very last step before returning, the compiler or interpreter may optimize it so that its space efficiency is comparable to iteration. This case is called tail recursion.

    • Regular recursion: When a function returns to the previous level, it needs to continue executing code, so the system needs to save the context of the previous layer's call.
    • Tail recursion: The recursive call is the last operation before the function returns, meaning that after returning to the previous level, there is no need to continue executing other operations, so the system does not need to save the context of the previous layer's function.

    Taking the calculation of \\(1 + 2 + \\dots + n\\) as an example, we can set the result variable res as a function parameter to implement tail recursion:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
    def tail_recur(n, res):\n    \"\"\"Tail recursion\"\"\"\n    # Termination condition\n    if n == 0:\n        return res\n    # Tail recursive call\n    return tail_recur(n - 1, res + n)\n
    recursion.cpp
    /* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.java
    /* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.cs
    /* Tail recursion */\nint TailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return TailRecur(n - 1, res + n);\n}\n
    recursion.go
    /* Tail recursion */\nfunc tailRecur(n int, res int) int {\n    // Termination condition\n    if n == 0 {\n        return res\n    }\n    // Tail recursive call\n    return tailRecur(n-1, res+n)\n}\n
    recursion.swift
    /* Tail recursion */\nfunc tailRecur(n: Int, res: Int) -> Int {\n    // Termination condition\n    if n == 0 {\n        return res\n    }\n    // Tail recursive call\n    return tailRecur(n: n - 1, res: res + n)\n}\n
    recursion.js
    /* Tail recursion */\nfunction tailRecur(n, res) {\n    // Termination condition\n    if (n === 0) return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.ts
    /* Tail recursion */\nfunction tailRecur(n: number, res: number): number {\n    // Termination condition\n    if (n === 0) return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.dart
    /* Tail recursion */\nint tailRecur(int n, int res) {\n  // Termination condition\n  if (n == 0) return res;\n  // Tail recursive call\n  return tailRecur(n - 1, res + n);\n}\n
    recursion.rs
    /* Tail recursion */\nfn tail_recur(n: i32, res: i32) -> i32 {\n    // Termination condition\n    if n == 0 {\n        return res;\n    }\n    // Tail recursive call\n    tail_recur(n - 1, res + n)\n}\n
    recursion.c
    /* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.kt
    /* Tail recursion */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n    // Add tailrec keyword to enable tail recursion optimization\n    // Termination condition\n    if (n == 0)\n        return res\n    // Tail recursive call\n    return tailRecur(n - 1, res + n)\n}\n
    recursion.rb
    ### Tail recursion ###\ndef tail_recur(n, res)\n  # Termination condition\n  return res if n == 0\n  # Tail recursive call\n  tail_recur(n - 1, res + n)\nend\n

    The execution process of tail recursion is shown in Figure 2-5. Comparing regular recursion and tail recursion, the summation operation is performed at different points.

    • Regular recursion: The summation operation is performed during the \"ascending\" process, requiring an additional summation operation after each layer returns.
    • Tail recursion: The summation operation is performed during the \"descending\" process; the \"ascending\" process only needs to return layer by layer.

    Figure 2-5   Tail recursion process

    Tip

    Please note that many compilers or interpreters do not support tail recursion optimization. For example, Python does not support tail recursion optimization by default, so even if a function is in tail recursive form, it may still encounter stack overflow issues.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3-recursion-tree","level":3,"title":"3.   Recursion Tree","text":"

    When dealing with algorithmic problems related to \"divide and conquer\", recursion often provides a more intuitive approach and more readable code than iteration. Taking the \"Fibonacci sequence\" as an example.

    Question

    Given a Fibonacci sequence \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\), find the \\(n\\)-th number in the sequence.

    Let the \\(n\\)-th number of the Fibonacci sequence be \\(f(n)\\). Two conclusions can be easily obtained.

    • The first two numbers of the sequence are \\(f(1) = 0\\) and \\(f(2) = 1\\).
    • Each number in the sequence is the sum of the previous two numbers, i.e., \\(f(n) = f(n - 1) + f(n - 2)\\).

    Following the recurrence relation to make recursive calls, with the first two numbers as termination conditions, we can write the recursive code. Calling fib(n) will give us the \\(n\\)-th number of the Fibonacci sequence:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
    def fib(n: int) -> int:\n    \"\"\"Fibonacci sequence: recursion\"\"\"\n    # Termination condition f(1) = 0, f(2) = 1\n    if n == 1 or n == 2:\n        return n - 1\n    # Recursive call f(n) = f(n-1) + f(n-2)\n    res = fib(n - 1) + fib(n - 2)\n    # Return result f(n)\n    return res\n
    recursion.cpp
    /* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.java
    /* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.cs
    /* Fibonacci sequence: recursion */\nint Fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = Fib(n - 1) + Fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.go
    /* Fibonacci sequence: recursion */\nfunc fib(n int) int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    res := fib(n-1) + fib(n-2)\n    // Return result f(n)\n    return res\n}\n
    recursion.swift
    /* Fibonacci sequence: recursion */\nfunc fib(n: Int) -> Int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    let res = fib(n: n - 1) + fib(n: n - 2)\n    // Return result f(n)\n    return res\n}\n
    recursion.js
    /* Fibonacci sequence: recursion */\nfunction fib(n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.ts
    /* Fibonacci sequence: recursion */\nfunction fib(n: number): number {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.dart
    /* Fibonacci sequence: recursion */\nint fib(int n) {\n  // Termination condition f(1) = 0, f(2) = 1\n  if (n == 1 || n == 2) return n - 1;\n  // Recursive call f(n) = f(n-1) + f(n-2)\n  int res = fib(n - 1) + fib(n - 2);\n  // Return result f(n)\n  return res;\n}\n
    recursion.rs
    /* Fibonacci sequence: recursion */\nfn fib(n: i32) -> i32 {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1;\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    let res = fib(n - 1) + fib(n - 2);\n    // Return result\n    res\n}\n
    recursion.c
    /* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.kt
    /* Fibonacci sequence: recursion */\nfun fib(n: Int): Int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    val res = fib(n - 1) + fib(n - 2)\n    // Return result f(n)\n    return res\n}\n
    recursion.rb
    ### Fibonacci sequence: recursion ###\ndef fib(n)\n  # Termination condition f(1) = 0, f(2) = 1\n  return n - 1 if n == 1 || n == 2\n  # Recursive call f(n) = f(n-1) + f(n-2)\n  res = fib(n - 1) + fib(n - 2)\n  # Return result f(n)\n  res\nend\n

    Observing the above code, we make two recursive calls within the function, meaning that one call produces two call branches. As shown in Figure 2-6, this repeated recursive calling eventually produces a recursion tree with \\(n\\) levels.

    Figure 2-6   Recursion tree of the Fibonacci sequence

    Fundamentally, recursion embodies the paradigm of \"decomposing a problem into smaller subproblems\", and this divide-and-conquer strategy is crucial.

    • From an algorithmic perspective, many important algorithmic strategies such as searching, sorting, backtracking, divide and conquer, and dynamic programming directly or indirectly apply this way of thinking.
    • From a data structure perspective, recursion is naturally suited for handling problems related to linked lists, trees, and graphs, because they are well-suited for analysis using divide-and-conquer thinking.
    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223-comparison-of-the-two","level":2,"title":"2.2.3   Comparison of the Two","text":"

    Summarizing the above content, as shown in Table 2-1, iteration and recursion differ in implementation, performance, and applicability.

    Table 2-1   Comparison of iteration and recursion characteristics

    Iteration Recursion Implementation Loop structure Function calls itself Time efficiency Generally more efficient, no function call overhead Each function call incurs overhead Memory usage Usually uses a fixed amount of memory space Accumulated function calls may use a large amount of stack frame space Suitable problems Suitable for simple loop tasks, with intuitive and readable code Suitable for subproblem decomposition, such as trees, graphs, divide and conquer, backtracking, etc., with concise and clear code structure

    Tip

    If you find the following content difficult to understand, you can review it after reading the \"Stack\" chapter.

    What is the intrinsic relationship between iteration and recursion? Taking the above recursive function as an example, the summation operation is performed during the \"ascending\" phase of recursion. This means that the function called first actually completes its summation operation last, and this working mechanism is similar to the \"last-in, first-out\" principle of stacks.

    In fact, recursive terminology such as \"call stack\" and \"stack frame space\" already hints at the close relationship between recursion and stacks.

    1. Descend: When a function is called, the system allocates a new stack frame on the \"call stack\" for that function to store the function's local variables, parameters, return address, and other data.
    2. Ascend: When the function completes execution and returns, the corresponding stack frame is removed from the \"call stack\", restoring the execution environment of the previous function.

    Therefore, we can use an explicit stack to simulate the behavior of the call stack, thus transforming recursion into iterative form:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
    def for_loop_recur(n: int) -> int:\n    \"\"\"Simulate recursion using iteration\"\"\"\n    # Use an explicit stack to simulate the system call stack\n    stack = []\n    res = 0\n    # Recurse: recursive call\n    for i in range(n, 0, -1):\n        # Simulate \"recurse\" with \"push\"\n        stack.append(i)\n    # Return: return result\n    while stack:\n        # Simulate \"return\" with \"pop\"\n        res += stack.pop()\n    # res = 1+2+3+...+n\n    return res\n
    recursion.cpp
    /* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    stack<int> stack;\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (!stack.empty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.top();\n        stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.java
    /* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    Stack<Integer> stack = new Stack<>();\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (!stack.isEmpty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.cs
    /* Simulate recursion using iteration */\nint ForLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    Stack<int> stack = new();\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.Push(i);\n    }\n    // Return: return result\n    while (stack.Count > 0) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.Pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.go
    /* Simulate recursion using iteration */\nfunc forLoopRecur(n int) int {\n    // Use an explicit stack to simulate the system call stack\n    stack := list.New()\n    res := 0\n    // Recurse: recursive call\n    for i := n; i > 0; i-- {\n        // Simulate \"recurse\" with \"push\"\n        stack.PushBack(i)\n    }\n    // Return: return result\n    for stack.Len() != 0 {\n        // Simulate \"return\" with \"pop\"\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
    /* Simulate recursion using iteration */\nfunc forLoopRecur(n: Int) -> Int {\n    // Use an explicit stack to simulate the system call stack\n    var stack: [Int] = []\n    var res = 0\n    // Recurse: recursive call\n    for i in (1 ... n).reversed() {\n        // Simulate \"recurse\" with \"push\"\n        stack.append(i)\n    }\n    // Return: return result\n    while !stack.isEmpty {\n        // Simulate \"return\" with \"pop\"\n        res += stack.removeLast()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
    recursion.js
    /* Simulate recursion using iteration */\nfunction forLoopRecur(n) {\n    // Use an explicit stack to simulate the system call stack\n    const stack = [];\n    let res = 0;\n    // Recurse: recursive call\n    for (let i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (stack.length) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.ts
    /* Simulate recursion using iteration */\nfunction forLoopRecur(n: number): number {\n    // Use an explicit stack to simulate the system call stack\n    const stack: number[] = [];\n    let res: number = 0;\n    // Recurse: recursive call\n    for (let i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (stack.length) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.dart
    /* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n  // Use an explicit stack to simulate the system call stack\n  List<int> stack = [];\n  int res = 0;\n  // Recurse: recursive call\n  for (int i = n; i > 0; i--) {\n    // Simulate \"recurse\" with \"push\"\n    stack.add(i);\n  }\n  // Return: return result\n  while (!stack.isEmpty) {\n    // Simulate \"return\" with \"pop\"\n    res += stack.removeLast();\n  }\n  // res = 1+2+3+...+n\n  return res;\n}\n
    recursion.rs
    /* Simulate recursion using iteration */\nfn for_loop_recur(n: i32) -> i32 {\n    // Use an explicit stack to simulate the system call stack\n    let mut stack = Vec::new();\n    let mut res = 0;\n    // Recurse: recursive call\n    for i in (1..=n).rev() {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while !stack.is_empty() {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop().unwrap();\n    }\n    // res = 1+2+3+...+n\n    res\n}\n
    recursion.c
    /* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    int stack[1000]; // Use a large array to simulate stack\n    int top = -1;    // Stack top index\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack[1 + top++] = i;\n    }\n    // Return: return result\n    while (top >= 0) {\n        // Simulate \"return\" with \"pop\"\n        res += stack[top--];\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.kt
    /* Simulate recursion using iteration */\nfun forLoopRecur(n: Int): Int {\n    // Use an explicit stack to simulate the system call stack\n    val stack = Stack<Int>()\n    var res = 0\n    // Descend: recursive call\n    for (i in n downTo 0) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i)\n    }\n    // Return: return result\n    while (stack.isNotEmpty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
    recursion.rb
    ### Use iteration to simulate recursion ###\ndef for_loop_recur(n)\n  # Use an explicit stack to simulate the system call stack\n  stack = []\n  res = 0\n\n  # Recurse: recursive call\n  for i in n.downto(0)\n    # Simulate \"recurse\" with \"push\"\n    stack << i\n  end\n  # Return: return result\n  while !stack.empty?\n    res += stack.pop\n  end\n\n  # res = 1+2+3+...+n\n  res\nend\n

    Observing the above code, when recursion is transformed into iteration, the code becomes more complex. Although iteration and recursion can be converted into each other in many cases, it may not be worthwhile to do so for the following two reasons.

    • The transformed code may be more difficult to understand and less readable.
    • For some complex problems, simulating the behavior of the system call stack can be very difficult.

    In summary, choosing between iteration and recursion depends on the nature of the specific problem. In programming practice, it is crucial to weigh the pros and cons of both and choose the appropriate method based on the context.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   Algorithm Efficiency Evaluation","text":"

    In algorithm design, we pursue the following two levels of objectives sequentially.

    1. Finding a solution to the problem: The algorithm must reliably obtain the correct solution within the specified input range.
    2. Seeking the optimal solution: Multiple solutions may exist for the same problem, and we hope to find an algorithm that is as efficient as possible.

    In other words, under the premise of being able to solve the problem, algorithm efficiency has become the primary evaluation criterion for measuring the quality of algorithms. It includes the following two dimensions.

    • Time efficiency: The length of time the algorithm runs.
    • Space efficiency: The size of memory space the algorithm occupies.

    In short, our goal is to design data structures and algorithms that are \"both fast and memory-efficient\". Effectively evaluating algorithm efficiency is crucial, because only in this way can we compare various algorithms and guide the algorithm design and optimization process.

    Efficiency evaluation methods are mainly divided into two types: actual testing and theoretical estimation.

    ","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211-actual-testing","level":2,"title":"2.1.1   Actual Testing","text":"

    Suppose we now have algorithm A and algorithm B, both of which can solve the same problem, and we need to compare their efficiency. The most direct method is to run them on a computer and measure their running time and memory usage. This evaluation approach can reflect real-world behavior, but it also has considerable limitations.

    On one hand, it is difficult to eliminate interference factors from the testing environment. Hardware configuration affects algorithmic performance. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm performs memory-intensive operations, it will benefit more from high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical.

    On the other hand, conducting complete testing is very resource-intensive. As the input data volume changes, the algorithm will exhibit different efficiencies. For example, when the input data volume is small, the running time of algorithm A is shorter than algorithm B; but when the input data volume is large, the test results may be exactly the opposite. Therefore, to obtain convincing conclusions, we need to test input data of various scales, which requires a large amount of computational resources.

    ","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212-theoretical-estimation","level":2,"title":"2.1.2   Theoretical Estimation","text":"

    Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through theoretical calculation. This estimation method is called asymptotic complexity analysis, or complexity analysis for short.

    Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. It describes the growth trend of the time and space required for algorithm execution as the input data scale increases. This definition is a bit cumbersome, so we can break it down into three key points to understand.

    • \"Time and space resources\" correspond to time complexity and space complexity, respectively.
    • \"As the input data scale increases\" means that complexity reflects the relationship between algorithm running efficiency and input data scale.
    • \"Growth trend of time and space\" indicates that complexity analysis focuses not on the specific values of running time or occupied space, but on how \"fast\" time or space grows.

    Complexity analysis overcomes the drawbacks of the actual testing method, reflected in the following aspects.

    • It does not need to actually run the code, making it more environmentally friendly and energy-efficient.
    • It is independent of the testing environment, and the analysis results are applicable to all running platforms.
    • It can reflect algorithm efficiency at different data volumes, especially algorithm performance at large data volumes.

    Tip

    If you are still confused about the concept of complexity, don't worry—we will introduce it in detail in subsequent chapters.

    Complexity analysis provides us with a \"ruler\" for evaluating algorithm efficiency, allowing us to measure the time and space resources required to execute a certain algorithm and compare the efficiency between different algorithms.

    Complexity is a mathematical concept that may feel abstract and challenging for beginners. From this perspective, complexity analysis may not be the most suitable topic to introduce first. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage.

    In summary, it is recommended that before diving deep into data structures and algorithms, you first establish a preliminary understanding of complexity analysis so that you can analyze the complexity of simple algorithms.

    ","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   Space Complexity","text":"

    Space complexity measures the growth trend of memory space occupied by an algorithm as the data size increases. This concept is very similar to time complexity, except that \"running time\" is replaced with \"occupied memory space\".

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241-algorithm-related-space","level":2,"title":"2.4.1   Algorithm-Related Space","text":"

    The memory space used by an algorithm during execution mainly includes the following types.

    • Input space: Used to store the input data of the algorithm.
    • Temporary space: Used to store variables, objects, function contexts, and other data during the algorithm's execution.
    • Output space: Used to store the output data of the algorithm.

    In general, the scope of space complexity statistics is \"temporary space\" plus \"output space\".

    Temporary space can be further divided into three parts.

    • Temporary data: Used to save various constants, variables, objects, etc., during the algorithm's execution.
    • Stack frame space: Used to save the context data of called functions. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns.
    • Instruction space: Used to save compiled program instructions, which are usually ignored in actual statistics.

    When analyzing the space complexity of a program, we usually consider three parts: temporary data, stack frame space, and output data, as shown in the following figure.

    Figure 2-15   Algorithm-related space

    The related code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class Node:\n    \"\"\"Class\"\"\"\n    def __init__(self, x: int):\n        self.val: int = x              # Node value\n        self.next: Node | None = None  # Reference to the next node\n\ndef function() -> int:\n    \"\"\"Function\"\"\"\n    # Perform some operations...\n    return 0\n\ndef algorithm(n) -> int:  # Input data\n    A = 0                 # Temporary data (constant, usually represented by uppercase letters)\n    b = 0                 # Temporary data (variable)\n    node = Node(0)        # Temporary data (object)\n    c = function()        # Stack frame space (function call)\n    return A + b + c      # Output data\n
    /* Structure */\nstruct Node {\n    int val;\n    Node *next;\n    Node(int x) : val(x), next(nullptr) {}\n};\n\n/* Function */\nint func() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) {        // Input data\n    const int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node* node = new Node(0); // Temporary data (object)\n    int c = func();           // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
    /* Class */\nclass Node {\n    int val;\n    Node next;\n    Node(int x) { val = x; }\n}\n\n/* Function */\nint function() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) {        // Input data\n    final int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node node = new Node(0);  // Temporary data (object)\n    int c = function();       // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
    /* Class */\nclass Node(int x) {\n    int val = x;\n    Node next;\n}\n\n/* Function */\nint Function() {\n    // Perform some operations...\n    return 0;\n}\n\nint Algorithm(int n) {        // Input data\n    const int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node node = new(0);       // Temporary data (object)\n    int c = Function();       // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
    /* Structure */\ntype node struct {\n    val  int\n    next *node\n}\n\n/* Create node structure */\nfunc newNode(val int) *node {\n    return &node{val: val}\n}\n\n/* Function */\nfunc function() int {\n    // Perform some operations...\n    return 0\n}\n\nfunc algorithm(n int) int { // Input data\n    const a = 0             // Temporary data (constant)\n    b := 0                  // Temporary data (variable)\n    newNode(0)              // Temporary data (object)\n    c := function()         // Stack frame space (function call)\n    return a + b + c        // Output data\n}\n
    /* Class */\nclass Node {\n    var val: Int\n    var next: Node?\n\n    init(x: Int) {\n        val = x\n    }\n}\n\n/* Function */\nfunc function() -> Int {\n    // Perform some operations...\n    return 0\n}\n\nfunc algorithm(n: Int) -> Int { // Input data\n    let a = 0             // Temporary data (constant)\n    var b = 0             // Temporary data (variable)\n    let node = Node(x: 0) // Temporary data (object)\n    let c = function()    // Stack frame space (function call)\n    return a + b + c      // Output data\n}\n
    /* Class */\nclass Node {\n    val;\n    next;\n    constructor(val) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.next = null;                       // Reference to the next node\n    }\n}\n\n/* Function */\nfunction constFunc() {\n    // Perform some operations\n    return 0;\n}\n\nfunction algorithm(n) {       // Input data\n    const a = 0;              // Temporary data (constant)\n    let b = 0;                // Temporary data (variable)\n    const node = new Node(0); // Temporary data (object)\n    const c = constFunc();    // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
    /* Class */\nclass Node {\n    val: number;\n    next: Node | null;\n    constructor(val?: number) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.next = null;                       // Reference to the next node\n    }\n}\n\n/* Function */\nfunction constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n\nfunction algorithm(n: number): number { // Input data\n    const a = 0;                        // Temporary data (constant)\n    let b = 0;                          // Temporary data (variable)\n    const node = new Node(0);           // Temporary data (object)\n    const c = constFunc();              // Stack frame space (function call)\n    return a + b + c;                   // Output data\n}\n
    /* Class */\nclass Node {\n  int val;\n  Node next;\n  Node(this.val, [this.next]);\n}\n\n/* Function */\nint function() {\n  // Perform some operations...\n  return 0;\n}\n\nint algorithm(int n) {  // Input data\n  const int a = 0;      // Temporary data (constant)\n  int b = 0;            // Temporary data (variable)\n  Node node = Node(0);  // Temporary data (object)\n  int c = function();   // Stack frame space (function call)\n  return a + b + c;     // Output data\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Structure */\nstruct Node {\n    val: i32,\n    next: Option<Rc<RefCell<Node>>>,\n}\n\n/* Create Node structure */\nimpl Node {\n    fn new(val: i32) -> Self {\n        Self { val: val, next: None }\n    }\n}\n\n/* Function */\nfn function() -> i32 {\n    // Perform some operations...\n    return 0;\n}\n\nfn algorithm(n: i32) -> i32 {       // Input data\n    const a: i32 = 0;               // Temporary data (constant)\n    let mut b = 0;                  // Temporary data (variable)\n    let node = Node::new(0);        // Temporary data (object)\n    let c = function();             // Stack frame space (function call)\n    return a + b + c;               // Output data\n}\n
    /* Function */\nint func() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) { // Input data\n    const int a = 0;   // Temporary data (constant)\n    int b = 0;         // Temporary data (variable)\n    int c = func();    // Stack frame space (function call)\n    return a + b + c;  // Output data\n}\n
    /* Class */\nclass Node(var _val: Int) {\n    var next: Node? = null\n}\n\n/* Function */\nfun function(): Int {\n    // Perform some operations...\n    return 0\n}\n\nfun algorithm(n: Int): Int { // Input data\n    val a = 0                // Temporary data (constant)\n    var b = 0                // Temporary data (variable)\n    val node = Node(0)       // Temporary data (object)\n    val c = function()       // Stack frame space (function call)\n    return a + b + c         // Output data\n}\n
    ### Class ###\nclass Node\n    attr_accessor :val      # Node value\n    attr_accessor :next     # Reference to the next node\n\n    def initialize(x)\n        @val = x\n    end\nend\n\n### Function ###\ndef function\n    # Perform some operations...\n    0\nend\n\n### Algorithm ###\ndef algorithm(n)        # Input data\n    a = 0               # Temporary data (constant)\n    b = 0               # Temporary data (variable)\n    node = Node.new(0)  # Temporary data (object)\n    c = function        # Stack frame space (function call)\n    a + b + c           # Output data\nend\n
    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242-calculation-method","level":2,"title":"2.4.2   Calculation Method","text":"

    The calculation method for space complexity is roughly the same as for time complexity, except that what we measure changes from the \"number of operations\" to the \"amount of space used\".

    Unlike time complexity, we usually only focus on the worst-case space complexity. This is because memory space is a hard requirement, and we must ensure that sufficient memory space is reserved for all input data.

    Observe the following code. Here, \"worst case\" in worst-case space complexity has two meanings.

    1. Based on the worst input data: When \\(n < 10\\), the space complexity is \\(O(1)\\); but when \\(n > 10\\), the initialized array nums occupies \\(O(n)\\) space, so the worst-case space complexity is \\(O(n)\\).
    2. Based on the peak memory during algorithm execution: For example, before executing the last line, the program occupies \\(O(1)\\) space; when initializing the array nums, the program occupies \\(O(n)\\) space, so the worst-case space complexity is \\(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

    In recursive functions, it is necessary to count the stack frame space. Observe the following code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    def function() -> int:\n    # Perform some operations\n    return 0\n\ndef loop(n: int):\n    \"\"\"Loop has space complexity of O(1)\"\"\"\n    for _ in range(n):\n        function()\n\ndef recur(n: int):\n    \"\"\"Recursion has space complexity of O(n)\"\"\"\n    if n == 1:\n        return\n    return recur(n - 1)\n
    int func() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
    int function() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
    int Function() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid Loop(int n) {\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nint Recur(int n) {\n    if (n == 1) return 1;\n    return Recur(n - 1);\n}\n
    func function() int {\n    // Perform some operations\n    return 0\n}\n\n/* Loop has space complexity of O(1) */\nfunc loop(n int) {\n    for i := 0; i < n; i++ {\n        function()\n    }\n}\n\n/* Recursion has space complexity of O(n) */\nfunc recur(n int) {\n    if n == 1 {\n        return\n    }\n    recur(n - 1)\n}\n
    @discardableResult\nfunc function() -> Int {\n    // Perform some operations\n    return 0\n}\n\n/* Loop has space complexity of O(1) */\nfunc loop(n: Int) {\n    for _ in 0 ..< n {\n        function()\n    }\n}\n\n/* Recursion has space complexity of O(n) */\nfunc recur(n: Int) {\n    if n == 1 {\n        return\n    }\n    recur(n: n - 1)\n}\n
    function constFunc() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfunction loop(n) {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfunction recur(n) {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
    function constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfunction loop(n: number): void {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfunction recur(n: number): void {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
    int function() {\n  // Perform some operations\n  return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n  for (int i = 0; i < n; i++) {\n    function();\n  }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n  if (n == 1) return;\n  recur(n - 1);\n}\n
    fn function() -> i32 {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfn loop(n: i32) {\n    for i in 0..n {\n        function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfn recur(n: i32) {\n    if n == 1 {\n        return;\n    }\n    recur(n - 1);\n}\n
    int func() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
    fun function(): Int {\n    // Perform some operations\n    return 0\n}\n/* Loop has space complexity of O(1) */\nfun loop(n: Int) {\n    for (i in 0..<n) {\n        function()\n    }\n}\n/* Recursion has space complexity of O(n) */\nfun recur(n: Int) {\n    if (n == 1) return\n    return recur(n - 1)\n}\n
    def function\n    # Perform some operations\n    0\nend\n\n### Loop has space complexity of O(1) ###\ndef loop(n)\n    (0...n).each { function }\nend\n\n### Recursion has space complexity of O(n) ###\ndef recur(n)\n    return if n == 1\n    recur(n - 1)\nend\n

    The time complexity of both functions loop() and recur() is \\(O(n)\\), but their space complexities are different.

    • The function loop() calls function() \\(n\\) times in a loop. In each iteration, function() returns and releases its stack frame space, so the space complexity remains \\(O(1)\\).
    • The recursive function recur() has \\(n\\) unreturned recur() instances existing simultaneously during execution, thus occupying \\(O(n)\\) stack frame space.
    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243-common-types","level":2,"title":"2.4.3   Common Types","text":"

    Let the input data size be \\(n\\). The following figure shows common types of space complexity (arranged from low to high).

    \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline & \\text{Constant} < \\text{Logarithmic} < \\text{Linear} < \\text{Quadratic} < \\text{Exponential} \\end{aligned} \\]

    Figure 2-16   Common types of space complexity

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-constant-order-o1","level":3,"title":"1.   Constant Order \\(O(1)\\)","text":"

    Constant order is common for constants, variables, and objects whose number is independent of the input data size \\(n\\).

    It should be noted that memory occupied by initializing variables or calling functions in a loop is released when entering the next iteration, so it does not accumulate space, and the space complexity remains \\(O(1)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def function() -> int:\n    \"\"\"Function\"\"\"\n    # Perform some operations\n    return 0\n\ndef constant(n: int):\n    \"\"\"Constant order\"\"\"\n    # Constants, variables, objects occupy O(1) space\n    a = 0\n    nums = [0] * 10000\n    node = ListNode(0)\n    # Variables in the loop occupy O(1) space\n    for _ in range(n):\n        c = 0\n    # Functions in the loop occupy O(1) space\n    for _ in range(n):\n        function()\n
    space_complexity.cpp
    /* Function */\nint func() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    const int a = 0;\n    int b = 0;\n    vector<int> nums(10000);\n    ListNode node(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
    space_complexity.java
    /* Function */\nint function() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    final int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n
    space_complexity.cs
    /* Function */\nint Function() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid Constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n
    space_complexity.go
    /* Function */\nfunc function() int {\n    // Perform some operations...\n    return 0\n}\n\n/* Constant order */\nfunc spaceConstant(n int) {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0\n    b := 0\n    nums := make([]int, 10000)\n    node := newNode(0)\n    // Variables in the loop occupy O(1) space\n    var c int\n    for i := 0; i < n; i++ {\n        c = 0\n    }\n    // Functions in the loop occupy O(1) space\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
    /* Function */\n@discardableResult\nfunc function() -> Int {\n    // Perform some operations\n    return 0\n}\n\n/* Constant order */\nfunc constant(n: Int) {\n    // Constants, variables, objects occupy O(1) space\n    let a = 0\n    var b = 0\n    let nums = Array(repeating: 0, count: 10000)\n    let node = ListNode(x: 0)\n    // Variables in the loop occupy O(1) space\n    for _ in 0 ..< n {\n        let c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for _ in 0 ..< n {\n        function()\n    }\n}\n
    space_complexity.js
    /* Function */\nfunction constFunc() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nfunction constant(n) {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
    space_complexity.ts
    /* Function */\nfunction constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nfunction constant(n: number): void {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
    space_complexity.dart
    /* Function */\nint function() {\n  // Perform some operations\n  return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n  // Constants, variables, objects occupy O(1) space\n  final int a = 0;\n  int b = 0;\n  List<int> nums = List.filled(10000, 0);\n  ListNode node = ListNode(0);\n  // Variables in the loop occupy O(1) space\n  for (var i = 0; i < n; i++) {\n    int c = 0;\n  }\n  // Functions in the loop occupy O(1) space\n  for (var i = 0; i < n; i++) {\n    function();\n  }\n}\n
    space_complexity.rs
    /* Function */\nfn function() -> i32 {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\n#[allow(unused)]\nfn constant(n: i32) {\n    // Constants, variables, objects occupy O(1) space\n    const A: i32 = 0;\n    let b = 0;\n    let nums = vec![0; 10000];\n    let node = ListNode::new(0);\n    // Variables in the loop occupy O(1) space\n    for i in 0..n {\n        let c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for i in 0..n {\n        function();\n    }\n}\n
    space_complexity.c
    /* Function */\nint func() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    const int a = 0;\n    int b = 0;\n    int nums[1000];\n    ListNode *node = newListNode(0);\n    free(node);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
    space_complexity.kt
    /* Function */\nfun function(): Int {\n    // Perform some operations\n    return 0\n}\n\n/* Constant order */\nfun constant(n: Int) {\n    // Constants, variables, objects occupy O(1) space\n    val a = 0\n    var b = 0\n    val nums = Array(10000) { 0 }\n    val node = ListNode(0)\n    // Variables in the loop occupy O(1) space\n    for (i in 0..<n) {\n        val c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for (i in 0..<n) {\n        function()\n    }\n}\n
    space_complexity.rb
    ### Function ###\ndef function\n  # Perform some operations\n  0\nend\n\n### Constant time ###\ndef constant(n)\n  # Constants, variables, objects occupy O(1) space\n  a = 0\n  nums = [0] * 10000\n  node = ListNode.new\n\n  # Variables in the loop occupy O(1) space\n  (0...n).each { c = 0 }\n  # Functions in the loop occupy O(1) space\n  (0...n).each { function }\nend\n
    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-linear-order-on","level":3,"title":"2.   Linear Order \\(O(n)\\)","text":"

    Linear order is common in arrays, linked lists, stacks, queues, etc., where the number of elements is proportional to \\(n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def linear(n: int):\n    \"\"\"Linear order\"\"\"\n    # A list of length n occupies O(n) space\n    nums = [0] * n\n    # A hash table of length n occupies O(n) space\n    hmap = dict[int, str]()\n    for i in range(n):\n        hmap[i] = str(i)\n
    space_complexity.cpp
    /* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    vector<int> nums(n);\n    // A list of length n occupies O(n) space\n    vector<ListNode> nodes;\n    for (int i = 0; i < n; i++) {\n        nodes.push_back(ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    int[] nums = new int[n];\n    // A list of length n occupies O(n) space\n    List<ListNode> nodes = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        nodes.add(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nvoid Linear(int n) {\n    // Array of length n uses O(n) space\n    int[] nums = new int[n];\n    // A list of length n occupies O(n) space\n    List<ListNode> nodes = [];\n    for (int i = 0; i < n; i++) {\n        nodes.Add(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nfunc spaceLinear(n int) {\n    // Array of length n uses O(n) space\n    _ = make([]int, n)\n    // A list of length n occupies O(n) space\n    var nodes []*node\n    for i := 0; i < n; i++ {\n        nodes = append(nodes, newNode(i))\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nfunc linear(n: Int) {\n    // Array of length n uses O(n) space\n    let nums = Array(repeating: 0, count: n)\n    // A list of length n occupies O(n) space\n    let nodes = (0 ..< n).map { ListNode(x: $0) }\n    // A hash table of length n occupies O(n) space\n    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, \"\\($0)\") })\n}\n
    space_complexity.js
    /* Linear order */\nfunction linear(n) {\n    // Array of length n uses O(n) space\n    const nums = new Array(n);\n    // A list of length n occupies O(n) space\n    const nodes = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nfunction linear(n: number): void {\n    // Array of length n uses O(n) space\n    const nums = new Array(n);\n    // A list of length n occupies O(n) space\n    const nodes: ListNode[] = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nvoid linear(int n) {\n  // Array of length n uses O(n) space\n  List<int> nums = List.filled(n, 0);\n  // A list of length n occupies O(n) space\n  List<ListNode> nodes = [];\n  for (var i = 0; i < n; i++) {\n    nodes.add(ListNode(i));\n  }\n  // A hash table of length n occupies O(n) space\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
    /* Linear order */\n#[allow(unused)]\nfn linear(n: i32) {\n    // Array of length n uses O(n) space\n    let mut nums = vec![0; n as usize];\n    // A list of length n occupies O(n) space\n    let mut nodes = Vec::new();\n    for i in 0..n {\n        nodes.push(ListNode::new(i))\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Hash table */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Implemented using uthash.h\n} HashTable;\n\n/* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    int *nums = malloc(sizeof(int) * n);\n    free(nums);\n\n    // A list of length n occupies O(n) space\n    ListNode **nodes = malloc(sizeof(ListNode *) * n);\n    for (int i = 0; i < n; i++) {\n        nodes[i] = newListNode(i);\n    }\n    // Memory release\n    for (int i = 0; i < n; i++) {\n        free(nodes[i]);\n    }\n    free(nodes);\n\n    // A hash table of length n occupies O(n) space\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    // Memory release\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
    /* Linear order */\nfun linear(n: Int) {\n    // Array of length n uses O(n) space\n    val nums = Array(n) { 0 }\n    // A list of length n occupies O(n) space\n    val nodes = mutableListOf<ListNode>()\n    for (i in 0..<n) {\n        nodes.add(ListNode(i))\n    }\n    // A hash table of length n occupies O(n) space\n    val map = mutableMapOf<Int, String>()\n    for (i in 0..<n) {\n        map[i] = i.toString()\n    }\n}\n
    space_complexity.rb
    ### Linear time ###\ndef linear(n)\n  # A list of length n occupies O(n) space\n  nums = Array.new(n, 0)\n\n  # A hash table of length n occupies O(n) space\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n

    As shown in the following figure, the recursion depth of this function is \\(n\\), meaning that there are \\(n\\) unreturned linear_recur() functions existing simultaneously, using \\(O(n)\\) stack frame space:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def linear_recur(n: int):\n    \"\"\"Linear order (recursive implementation)\"\"\"\n    print(\"Recursion n =\", n)\n    if n == 1:\n        return\n    linear_recur(n - 1)\n
    space_complexity.cpp
    /* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    cout << \"Recursion n = \" << n << endl;\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
    space_complexity.java
    /* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    System.out.println(\"Recursion n = \" + n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
    space_complexity.cs
    /* Linear order (recursive implementation) */\nvoid LinearRecur(int n) {\n    Console.WriteLine(\"Recursion n = \" + n);\n    if (n == 1) return;\n    LinearRecur(n - 1);\n}\n
    space_complexity.go
    /* Linear order (recursive implementation) */\nfunc spaceLinearRecur(n int) {\n    fmt.Println(\"Recursion n =\", n)\n    if n == 1 {\n        return\n    }\n    spaceLinearRecur(n - 1)\n}\n
    space_complexity.swift
    /* Linear order (recursive implementation) */\nfunc linearRecur(n: Int) {\n    print(\"Recursion n = \\(n)\")\n    if n == 1 {\n        return\n    }\n    linearRecur(n: n - 1)\n}\n
    space_complexity.js
    /* Linear order (recursive implementation) */\nfunction linearRecur(n) {\n    console.log(`Recursion n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
    space_complexity.ts
    /* Linear order (recursive implementation) */\nfunction linearRecur(n: number): void {\n    console.log(`Recursion n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
    space_complexity.dart
    /* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n  print('Recursion n = $n');\n  if (n == 1) return;\n  linearRecur(n - 1);\n}\n
    space_complexity.rs
    /* Linear order (recursive implementation) */\nfn linear_recur(n: i32) {\n    println!(\"Recursion n = {}\", n);\n    if n == 1 {\n        return;\n    };\n    linear_recur(n - 1);\n}\n
    space_complexity.c
    /* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    printf(\"Recursion n = %d\\r\\n\", n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
    space_complexity.kt
    /* Linear order (recursive implementation) */\nfun linearRecur(n: Int) {\n    println(\"Recursion n = $n\")\n    if (n == 1)\n        return\n    linearRecur(n - 1)\n}\n
    space_complexity.rb
    ### Linear space (recursive) ###\ndef linear_recur(n)\n  puts \"Recursion n = #{n}\"\n  return if n == 1\n  linear_recur(n - 1)\nend\n

    Figure 2-17   Linear order space complexity generated by recursive function

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-quadratic-order-on2","level":3,"title":"3.   Quadratic Order \\(O(n^2)\\)","text":"

    Quadratic order is common in matrices and graphs, where the number of elements is quadratically related to \\(n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def quadratic(n: int):\n    \"\"\"Quadratic order\"\"\"\n    # A 2D list occupies O(n^2) space\n    num_matrix = [[0] * n for _ in range(n)]\n
    space_complexity.cpp
    /* Exponential order */\nvoid quadratic(int n) {\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nvoid quadratic(int n) {\n    // Matrix uses O(n^2) space\n    int[][] numMatrix = new int[n][n];\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nvoid Quadratic(int n) {\n    // Matrix uses O(n^2) space\n    int[,] numMatrix = new int[n, n];\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nfunc spaceQuadratic(n int) {\n    // Matrix uses O(n^2) space\n    numMatrix := make([][]int, n)\n    for i := 0; i < n; i++ {\n        numMatrix[i] = make([]int, n)\n    }\n}\n
    space_complexity.swift
    /* Exponential order */\nfunc quadratic(n: Int) {\n    // 2D list uses O(n^2) space\n    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n
    space_complexity.js
    /* Exponential order */\nfunction quadratic(n) {\n    // Matrix uses O(n^2) space\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nfunction quadratic(n: number): void {\n    // Matrix uses O(n^2) space\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nvoid quadratic(int n) {\n  // Matrix uses O(n^2) space\n  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));\n  // 2D list uses O(n^2) space\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
    /* Exponential order */\n#[allow(unused)]\nfn quadratic(n: i32) {\n    // Matrix uses O(n^2) space\n    let num_matrix = vec![vec![0; n as usize]; n as usize];\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nvoid quadratic(int n) {\n    // 2D list uses O(n^2) space\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    // Memory release\n    for (int i = 0; i < n; i++) {\n        free(numMatrix[i]);\n    }\n    free(numMatrix);\n}\n
    space_complexity.kt
    /* Exponential order */\nfun quadratic(n: Int) {\n    // Matrix uses O(n^2) space\n    val numMatrix = arrayOfNulls<Array<Int>?>(n)\n    // 2D list uses O(n^2) space\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
    ### Quadratic time ###\ndef quadratic(n)\n  # 2D list uses O(n^2) space\n  Array.new(n) { Array.new(n, 0) }\nend\n

    As shown in the following figure, the recursion depth of this function is \\(n\\), and an array is initialized in each recursive function with lengths of \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\), with an average length of \\(n / 2\\), thus occupying \\(O(n^2)\\) space overall:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def quadratic_recur(n: int) -> int:\n    \"\"\"Quadratic order (recursive implementation)\"\"\"\n    if n <= 0:\n        return 0\n    # Array nums length is n, n-1, ..., 2, 1\n    nums = [0] * n\n    return quadratic_recur(n - 1)\n
    space_complexity.cpp
    /* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    vector<int> nums(n);\n    cout << \"In recursion n = \" << n << \", nums length = \" << nums.size() << endl;\n    return quadraticRecur(n - 1);\n}\n
    space_complexity.java
    /* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    // Array nums has length n, n-1, ..., 2, 1\n    int[] nums = new int[n];\n    System.out.println(\"In recursion n = \" + n + \", nums length = \" + nums.length);\n    return quadraticRecur(n - 1);\n}\n
    space_complexity.cs
    /* Quadratic order (recursive implementation) */\nint QuadraticRecur(int n) {\n    if (n <= 0) return 0;\n    int[] nums = new int[n];\n    Console.WriteLine(\"Recursion n = \" + n + \", nums length = \" + nums.Length);\n    return QuadraticRecur(n - 1);\n}\n
    space_complexity.go
    /* Quadratic order (recursive implementation) */\nfunc spaceQuadraticRecur(n int) int {\n    if n <= 0 {\n        return 0\n    }\n    nums := make([]int, n)\n    fmt.Printf(\"In recursion n = %d, nums length = %d \\n\", n, len(nums))\n    return spaceQuadraticRecur(n - 1)\n}\n
    space_complexity.swift
    /* Quadratic order (recursive implementation) */\n@discardableResult\nfunc quadraticRecur(n: Int) -> Int {\n    if n <= 0 {\n        return 0\n    }\n    // Array nums has length n, n-1, ..., 2, 1\n    let nums = Array(repeating: 0, count: n)\n    print(\"In recursion n = \\(n), nums length = \\(nums.count)\")\n    return quadraticRecur(n: n - 1)\n}\n
    space_complexity.js
    /* Quadratic order (recursive implementation) */\nfunction quadraticRecur(n) {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
    space_complexity.ts
    /* Quadratic order (recursive implementation) */\nfunction quadraticRecur(n: number): number {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
    space_complexity.dart
    /* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n  if (n <= 0) return 0;\n  List<int> nums = List.filled(n, 0);\n  print('In recursion n = $n, nums length = ${nums.length}');\n  return quadraticRecur(n - 1);\n}\n
    space_complexity.rs
    /* Quadratic order (recursive implementation) */\nfn quadratic_recur(n: i32) -> i32 {\n    if n <= 0 {\n        return 0;\n    };\n    // Array nums has length n, n-1, ..., 2, 1\n    let nums = vec![0; n as usize];\n    println!(\"In recursion n = {}, nums length = {}\", n, nums.len());\n    return quadratic_recur(n - 1);\n}\n
    space_complexity.c
    /* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    int *nums = malloc(sizeof(int) * n);\n    printf(\"In recursion n = %d, nums length = %d\\r\\n\", n, n);\n    int res = quadraticRecur(n - 1);\n    free(nums);\n    return res;\n}\n
    space_complexity.kt
    /* Quadratic order (recursive implementation) */\ntailrec fun quadraticRecur(n: Int): Int {\n    if (n <= 0)\n        return 0\n    // Array nums has length n, n-1, ..., 2, 1\n    val nums = Array(n) { 0 }\n    println(\"In recursion n = $n, nums length = ${nums.size}\")\n    return quadraticRecur(n - 1)\n}\n
    space_complexity.rb
    ### Quadratic space (recursive) ###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # Array nums has length n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n

    Figure 2-18   Quadratic order space complexity generated by recursive function

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-exponential-order-o2n","level":3,"title":"4.   Exponential Order \\(O(2^n)\\)","text":"

    Exponential order is common in binary trees. Observe the following figure: a \"full binary tree\" with \\(n\\) levels has \\(2^n - 1\\) nodes, occupying \\(O(2^n)\\) space:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def build_tree(n: int) -> TreeNode | None:\n    \"\"\"Exponential order (build full binary tree)\"\"\"\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    ### Exponential space (build full binary tree) ###\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

    Figure 2-19   Exponential order space complexity generated by full binary tree

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-logarithmic-order-olog-n","level":3,"title":"5.   Logarithmic Order \\(O(\\log n)\\)","text":"

    Logarithmic order is common in divide-and-conquer algorithms. For example, merge sort: given an input array of length \\(n\\), each recursion divides the array in half from the midpoint, forming a recursion tree of height \\(\\log n\\), using \\(O(\\log n)\\) stack frame space.

    Another example is converting a number to a string. Given a positive integer \\(n\\), it has \\(\\lfloor \\log_{10} n \\rfloor + 1\\) digits, i.e., the corresponding string length is \\(\\lfloor \\log_{10} n \\rfloor + 1\\), so the space complexity is \\(O(\\log_{10} n + 1) = O(\\log n)\\).

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244-trading-time-for-space","level":2,"title":"2.4.4   Trading Time for Space","text":"

    Ideally, we hope that both the time complexity and space complexity of an algorithm can reach optimal. However, in practice, optimizing both time complexity and space complexity simultaneously is usually very difficult.

    Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa. Sacrificing memory space to improve execution speed is called \"trading space for time\"; the reverse is called \"trading time for space\".

    The choice of which approach depends on which aspect we value more. In most cases, time is more precious than space, so \"trading space for time\" is usually the more common strategy. Of course, when the data volume is very large, controlling space complexity is also very important.

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   Summary","text":"","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"

    Algorithm Efficiency Assessment

    • Time efficiency and space efficiency are the two primary evaluation metrics for measuring algorithm performance.
    • We can evaluate algorithm efficiency through actual testing, but it is difficult to eliminate the influence of the testing environment, and it consumes substantial computational resources.
    • Complexity analysis can overcome the limitations of actual testing. Its results apply across running platforms, and it can reveal algorithm efficiency under different data scales.

    Time Complexity

    • Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but it may be less informative in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency.
    • Worst-case time complexity is represented using Big \\(O\\) notation, corresponding to the asymptotic upper bound of a function, reflecting the growth level of the number of operations \\(T(n)\\) as \\(n\\) approaches positive infinity.
    • Deriving time complexity involves two steps: first, counting the number of operations, then determining the asymptotic upper bound.
    • Common time complexities arranged from low to high include \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n \\log n)\\), \\(O(n^2)\\), \\(O(2^n)\\), and \\(O(n!)\\).
    • The time complexity of some algorithms is not fixed, but rather depends on the distribution of input data. Time complexity is divided into worst-case, best-case, and average-case time complexity. Best-case time complexity is rarely used because input data generally needs to satisfy strict conditions to achieve the best case.
    • Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires analyzing the input data distribution and the resulting mathematical expectation.

    Space Complexity

    • Space complexity serves a similar purpose to time complexity, used to measure the trend of algorithm memory usage as data volume increases.
    • The memory space related to algorithm execution can be divided into input space, temporary space, and output space. Typically, input space is not included in space complexity calculations. Temporary space can be divided into temporary data, stack frame space, and instruction space, where stack frame space usually affects space complexity only in recursive functions.
    • We typically only focus on worst-case space complexity, which is the space complexity of an algorithm under worst-case input data and worst-case runtime.
    • Common space complexities arranged from low to high include \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n^2)\\), and \\(O(2^n)\\).
    ","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Is the space complexity of tail recursion \\(O(1)\\)?

    Theoretically, the space complexity of tail recursive functions can be optimized to \\(O(1)\\). However, most programming languages (such as Java, Python, C++, Go, C#, etc.) do not support automatic tail recursion optimization, so the space complexity is generally considered to be \\(O(n)\\).

    Q: What is the difference between the terms function and method?

    A function can be executed independently, with all parameters passed explicitly. A method is associated with an object, is implicitly bound to the object that invokes it, and can operate on data contained in class instances.

    The following examples use several common programming languages for illustration.

    • C is a procedural programming language without object-oriented concepts, so it only has functions. However, we can simulate object-oriented programming by creating structures (struct), and functions associated with structures are equivalent to methods in other programming languages.
    • Java and C# are object-oriented programming languages where code blocks (methods) are typically part of a class. Static methods behave like functions because they are bound to the class and cannot access specific instance variables.
    • C++ and Python support both procedural programming (functions) and object-oriented programming (methods).

    Q: Does the diagram for \"common space complexity types\" reflect the absolute size of occupied space?

    No, the diagram shows space complexity, which reflects growth trends rather than the absolute size of occupied space.

    Assuming \\(n = 8\\), you might find that the values of each curve do not correspond to the functions. This is because each curve contains a constant term used to compress the value range into a visually comfortable range.

    In practice, because we generally do not know the \"constant-term\" cost of each method, we usually cannot choose the optimal solution for cases like \\(n = 8\\) based on complexity alone. But for \\(n = 8^5\\), the choice is straightforward, because the growth trend already dominates.

    Q: Are there situations where algorithms are designed to sacrifice time (or space) based on actual use cases?

    In practical applications, most situations choose to sacrifice space for time. For example, with database indexes, we typically choose to build B+ trees or hash indexes, occupying substantial memory space in exchange for efficient queries of \\(O(\\log n)\\) or even \\(O(1)\\).

    In scenarios where space resources are precious, time may be sacrificed for space. For example, in embedded development, device memory is precious, and engineers may forgo using hash tables and choose to use array sequential search to save memory usage, at the cost of slower searches.

    ","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   Time Complexity","text":"

    Runtime can intuitively and accurately reflect the efficiency of an algorithm. If we want to accurately estimate the runtime of a piece of code, how should we proceed?

    1. Determine the running platform, including hardware configuration, programming language, system environment, etc., as these factors all affect code execution efficiency.
    2. Evaluate the runtime required for various computational operations, for example, an addition operation + requires 1 ns, a multiplication operation * requires 10 ns, a print operation print() requires 5 ns, etc.
    3. Count all computational operations in the code, and sum the execution times of all operations to obtain the runtime.

    For example, in the following code, the input data size is \\(n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # On a certain running platform\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # Loop n times\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
    // On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
    // On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
    // On a certain running platform\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
    // On a certain running platform\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // Loop n times\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
    // On a certain running platform\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // Loop n times\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
    // On a certain running platform\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // Loop n times\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // On a certain running platform\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // Loop n times\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // On a certain running platform\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // Loop n times\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
    // On a certain running platform\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // Loop n times\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
    // On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
    // On a certain running platform\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // Loop n times\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
    # On a certain running platform\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # Loop n times\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

    According to the above method, the algorithm's runtime can be obtained as \\((6n + 12)\\) ns:

    \\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\]

    In reality, however, trying to count an algorithm's exact runtime is neither practical nor realistic. First, we do not want to tie the estimated time to the running platform, because algorithms need to run on many different platforms. Second, it is difficult to know the runtime of each type of operation, which makes the estimation process extremely difficult.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231-counting-time-growth-trends","level":2,"title":"2.3.1   Counting Time Growth Trends","text":"

    Time complexity analysis does not count the algorithm's runtime, but rather counts the growth trend of the algorithm's runtime as the data volume increases.

    The concept of \"time growth trend\" is rather abstract; let us understand it through an example. Suppose the input data size is \\(n\\), and given three algorithms A, B, and C:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # Time complexity of algorithm A: constant order\ndef algorithm_A(n: int):\n    print(0)\n# Time complexity of algorithm B: linear order\ndef algorithm_B(n: int):\n    for _ in range(n):\n        print(0)\n# Time complexity of algorithm C: constant order\ndef algorithm_C(n: int):\n    for _ in range(1000000):\n        print(0)\n
    // Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    cout << 0 << endl;\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        cout << 0 << endl;\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        cout << 0 << endl;\n    }\n}\n
    // Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    System.out.println(0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        System.out.println(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        System.out.println(0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nvoid AlgorithmA(int n) {\n    Console.WriteLine(0);\n}\n// Time complexity of algorithm B: linear order\nvoid AlgorithmB(int n) {\n    for (int i = 0; i < n; i++) {\n        Console.WriteLine(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid AlgorithmC(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        Console.WriteLine(0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfunc algorithm_A(n int) {\n    fmt.Println(0)\n}\n// Time complexity of algorithm B: linear order\nfunc algorithm_B(n int) {\n    for i := 0; i < n; i++ {\n        fmt.Println(0)\n    }\n}\n// Time complexity of algorithm C: constant order\nfunc algorithm_C(n int) {\n    for i := 0; i < 1000000; i++ {\n        fmt.Println(0)\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfunc algorithmA(n: Int) {\n    print(0)\n}\n\n// Time complexity of algorithm B: linear order\nfunc algorithmB(n: Int) {\n    for _ in 0 ..< n {\n        print(0)\n    }\n}\n\n// Time complexity of algorithm C: constant order\nfunc algorithmC(n: Int) {\n    for _ in 0 ..< 1_000_000 {\n        print(0)\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfunction algorithm_A(n) {\n    console.log(0);\n}\n// Time complexity of algorithm B: linear order\nfunction algorithm_B(n) {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfunction algorithm_C(n) {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfunction algorithm_A(n: number): void {\n    console.log(0);\n}\n// Time complexity of algorithm B: linear order\nfunction algorithm_B(n: number): void {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfunction algorithm_C(n: number): void {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nvoid algorithmA(int n) {\n  print(0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithmB(int n) {\n  for (int i = 0; i < n; i++) {\n    print(0);\n  }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithmC(int n) {\n  for (int i = 0; i < 1000000; i++) {\n    print(0);\n  }\n}\n
    // Time complexity of algorithm A: constant order\nfn algorithm_A(n: i32) {\n    println!(\"{}\", 0);\n}\n// Time complexity of algorithm B: linear order\nfn algorithm_B(n: i32) {\n    for _ in 0..n {\n        println!(\"{}\", 0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfn algorithm_C(n: i32) {\n    for _ in 0..1000000 {\n        println!(\"{}\", 0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    printf(\"%d\", 0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        printf(\"%d\", 0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        printf(\"%d\", 0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfun algoritm_A(n: Int) {\n    println(0)\n}\n// Time complexity of algorithm B: linear order\nfun algorithm_B(n: Int) {\n    for (i in 0..<n){\n        println(0)\n    }\n}\n// Time complexity of algorithm C: constant order\nfun algorithm_C(n: Int) {\n    for (i in 0..<1000000) {\n        println(0)\n    }\n}\n
    # Time complexity of algorithm A: constant order\ndef algorithm_A(n)\n    puts 0\nend\n\n# Time complexity of algorithm B: linear order\ndef algorithm_B(n)\n    (0...n).each { puts 0 }\nend\n\n# Time complexity of algorithm C: constant order\ndef algorithm_C(n)\n    (0...1_000_000).each { puts 0 }\nend\n

    Figure 2-7 shows the time complexity of the above three algorithm functions.

    • Algorithm A has only \\(1\\) print operation, and the algorithm's runtime does not grow as \\(n\\) increases. We call the time complexity of this algorithm \"constant order\".
    • In algorithm B, the print operation needs to loop \\(n\\) times, and the algorithm's runtime grows linearly as \\(n\\) increases. The time complexity of this algorithm is called \"linear order\".
    • In algorithm C, the print operation needs to loop \\(1000000\\) times. Although the runtime is very long, it is independent of the input data size \\(n\\). Therefore, the time complexity of C is the same as A, still \"constant order\".

    Figure 2-7   Time growth trends of algorithms A, B, and C

    Compared to directly counting the algorithm's runtime, what are the characteristics of time complexity analysis?

    • Time complexity can effectively evaluate algorithm efficiency. For example, the runtime of algorithm B grows linearly; when \\(n > 1\\) it is slower than algorithm A, and when \\(n > 1000000\\) it is slower than algorithm C. In fact, as long as the input data size \\(n\\) is sufficiently large, an algorithm with \"constant order\" complexity will always be superior to one with \"linear order\" complexity, which is precisely the meaning of time growth trend.
    • The derivation method for time complexity is simpler. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same \"unit time\", reducing \"tracking the runtime of each operation\" to \"counting the number of operations\", which greatly reduces the difficulty of estimation.
    • Time complexity also has certain limitations. For example, although algorithms A and C have the same time complexity, their actual runtimes differ significantly. Similarly, although algorithm B has a higher time complexity than C, when the input data size \\(n\\) is small, algorithm B is clearly superior to algorithm C. In such cases, it is often difficult to judge the efficiency of algorithms based solely on time complexity. Of course, despite the above issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency.
    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232-asymptotic-upper-bound-of-functions","level":2,"title":"2.3.2   Asymptotic Upper Bound of Functions","text":"

    Given a function with input size \\(n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    def algorithm(n: int):\n    a = 1      # +1\n    a = a + 1  # +1\n    a = a * 2  # +1\n    # Loop n times\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    // Loop n times\n    for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\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    // Loop n times\n    for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\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    // Loop n times\n    for (int i = 0; i < n; i++) {   // +1 (i++ is executed each round)\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    // Loop n times\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    // Loop n times\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    // Loop n times\n    for(let i = 0; i < n; i++){ // +1 (i++ is executed each round)\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    // Loop n times\n    for(let i = 0; i < n; i++){ // +1 (i++ is executed each round)\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  // Loop n times\n  for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\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    // Loop n times\n    for _ in 0..n { // +1 (i++ is executed each round)\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    // Loop n times\n    for (int i = 0; i < n; i++) {   // +1 (i++ is executed each round)\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    // Loop n times\n    for (i in 0..<n) { // +1 (i++ is executed each round)\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    # Loop n times\n    (0...n).each do # +1\n        puts 0      # +1\n    end\nend\n

    Let the number of operations of the algorithm be a function of the input data size \\(n\\), denoted as \\(T(n)\\). Then the number of operations of the above function is:

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

    \\(T(n)\\) is a linear function, indicating that its runtime growth trend is linear, and therefore its time complexity is linear order.

    We denote the time complexity of linear order as \\(O(n)\\). This mathematical symbol is called big-\\(O\\) notation, representing the asymptotic upper bound of the function \\(T(n)\\).

    Time complexity analysis essentially calculates the asymptotic upper bound of \"the number of operations \\(T(n)\\)\", which has a clear mathematical definition.

    Asymptotic upper bound of functions

    If there exist positive real numbers \\(c\\) and \\(n_0\\) such that for all \\(n > n_0\\), we have \\(T(n) \\leq c \\cdot f(n)\\), then \\(f(n)\\) can be considered as an asymptotic upper bound of \\(T(n)\\), denoted as \\(T(n) = O(f(n))\\).

    As shown in Figure 2-8, calculating the asymptotic upper bound is to find a function \\(f(n)\\) such that when \\(n\\) tends to infinity, \\(T(n)\\) and \\(f(n)\\) are at the same growth level, differing only by a constant coefficient \\(c\\).

    Figure 2-8   Asymptotic upper bound of a function

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233-derivation-method","level":2,"title":"2.3.3   Derivation Method","text":"

    The idea of an asymptotic upper bound is somewhat mathematical. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice.

    According to the definition, after determining \\(f(n)\\), we can obtain the time complexity \\(O(f(n))\\). So how do we determine the asymptotic upper bound \\(f(n)\\)? Overall, it is divided into two steps: first count the number of operations, then determine the asymptotic upper bound.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-step-1-count-the-number-of-operations","level":3,"title":"1.   Step 1: Count the Number of Operations","text":"

    For code, count from top to bottom line by line. However, since the constant coefficient \\(c\\) in \\(c \\cdot f(n)\\) above can be of any size, coefficients and constant terms in the number of operations \\(T(n)\\) can all be ignored. According to this principle, the following counting simplification techniques can be summarized.

    1. Ignore constants in \\(T(n)\\). Because they are all independent of \\(n\\), they do not affect time complexity.
    2. Omit all coefficients. For example, looping \\(2n\\) times, \\(5n + 1\\) times, etc., can all be simplified as \\(n\\) times, because the coefficient before \\(n\\) does not affect time complexity.
    3. Use multiplication for nested loops. The total number of operations equals the product of the number of operations in the outer and inner loops, with each layer of loop still able to apply techniques 1. and 2. separately.

    Given a function, we can use the above techniques to count the number of operations:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    def algorithm(n: int):\n    a = 1      # +0 (Technique 1)\n    a = a + n  # +0 (Technique 1)\n    # +n (Technique 2)\n    for i in range(5 * n + 1):\n        print(0)\n    # +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        cout << 0 << endl;\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        System.out.println(0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        Console.WriteLine(0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n  // +0 (Technique 1)\n    // +n (Technique 2)\n    for i := 0; i < 5 * n + 1; i++ {\n        fmt.Println(0)\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n // +0 (Technique 1)\n    // +n (Technique 2)\n    for _ in 0 ..< (5 * n + 1) {\n        print(0)\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n  a = a + n; // +0 (Technique 1)\n  // +n (Technique 2)\n  for (int i = 0; i < 5 * n + 1; i++) {\n    print(0);\n  }\n  // +n*n (Technique 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 (Technique 1)\n    a = a + n;        // +0 (Technique 1)\n\n    // +n (Technique 2)\n    for i in 0..(5 * n + 1) {\n        println!(\"{}\", 0);\n    }\n\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        printf(\"%d\", 0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n   // +0 (Technique 1)\n    // +n (Technique 2)\n    for (i in 0..<5 * n + 1) {\n        println(0)\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n   # +0 (Technique 1)\n    # +n (Technique 2)\n    (0...(5 * n + 1)).each do { puts 0 }\n    # +n*n (Technique 3)\n    (0...(2 * n)).each do\n        (0...(n + 1)).each do { puts 0 }\n    end\nend\n

    The following formula shows the counting results before and after using the above techniques; both derive a time complexity of \\(O(n^2)\\).

    \\[ \\begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \\text{Complete count (-.-|||)} \\newline & = 2n^2 + 7n + 3 \\newline T(n) & = n^2 + n & \\text{Simplified count (o.O)} \\end{aligned} \\]","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-step-2-determine-the-asymptotic-upper-bound","level":3,"title":"2.   Step 2: Determine the Asymptotic Upper Bound","text":"

    Time complexity is determined by the highest-order term in \\(T(n)\\). This is because as \\(n\\) tends to infinity, the highest-order term will play a dominant role, and the influence of other terms can be ignored.

    Table 2-2 shows some examples, where some exaggerated values are used to emphasize the conclusion that \"coefficients cannot shake the order\". When \\(n\\) tends to infinity, these constants become insignificant.

    Table 2-2   Time complexities corresponding to different numbers of operations

    Number of Operations \\(T(n)\\) Time Complexity \\(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":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234-common-types","level":2,"title":"2.3.4   Common Types","text":"

    Let the input data size be \\(n\\). Common time complexity types are shown in Figure 2-9 (arranged in order from low to high).

    \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n \\log n) < O(n^2) < O(2^n) < O(n!) \\newline & \\text{Constant} < \\text{Logarithmic} < \\text{Linear} < \\text{Linearithmic} < \\text{Quadratic} < \\text{Exponential} < \\text{Factorial} \\end{aligned} \\]

    Figure 2-9   Common time complexity types

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-constant-order-o1","level":3,"title":"1.   Constant Order \\(O(1)\\)","text":"

    The number of operations in constant order is independent of the input data size \\(n\\), meaning it does not change as \\(n\\) changes.

    In the following function, although the value of size may be large, it is independent of the input data size \\(n\\), so the time complexity remains \\(O(1)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def constant(n: int) -> int:\n    \"\"\"Constant order\"\"\"\n    count = 0\n    size = 100000\n    for _ in range(size):\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    ### Constant time ###\ndef constant(n)\n  count = 0\n  size = 100000\n\n  (0...size).each { count += 1 }\n\n  count\nend\n
    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-linear-order-on","level":3,"title":"2.   Linear Order \\(O(n)\\)","text":"

    The number of operations in linear order grows linearly relative to the input data size \\(n\\). Linear order typically appears in single-layer loops:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def linear(n: int) -> int:\n    \"\"\"Linear order\"\"\"\n    count = 0\n    for _ in range(n):\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\nfunction linear(n) {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
    time_complexity.ts
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    ### Linear time ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n

    Operations such as traversing arrays and traversing linked lists have a time complexity of \\(O(n)\\), where \\(n\\) is the length of the array or linked list:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def array_traversal(nums: list[int]) -> int:\n    \"\"\"Linear order (traversing array)\"\"\"\n    count = 0\n    # Number of iterations is proportional to the array length\n    for num in nums:\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Linear order (traversing array) */\nint arrayTraversal(vector<int> &nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.java
    /* Linear order (traversing array) */\nint arrayTraversal(int[] nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.cs
    /* Linear order (traversing array) */\nint ArrayTraversal(int[] nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    foreach (int num in nums) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.go
    /* Linear order (traversing array) */\nfunc arrayTraversal(nums []int) int {\n    count := 0\n    // Number of iterations is proportional to the array length\n    for range nums {\n        count++\n    }\n    return count\n}\n
    time_complexity.swift
    /* Linear order (traversing array) */\nfunc arrayTraversal(nums: [Int]) -> Int {\n    var count = 0\n    // Number of iterations is proportional to the array length\n    for _ in nums {\n        count += 1\n    }\n    return count\n}\n
    time_complexity.js
    /* Linear order (traversing array) */\nfunction arrayTraversal(nums) {\n    let count = 0;\n    // Number of iterations is proportional to the array length\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.ts
    /* Linear order (traversing array) */\nfunction arrayTraversal(nums: number[]): number {\n    let count = 0;\n    // Number of iterations is proportional to the array length\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.dart
    /* Linear order (traversing array) */\nint arrayTraversal(List<int> nums) {\n  int count = 0;\n  // Number of iterations is proportional to the array length\n  for (var _num in nums) {\n    count++;\n  }\n  return count;\n}\n
    time_complexity.rs
    /* Linear order (traversing array) */\nfn array_traversal(nums: &[i32]) -> i32 {\n    let mut count = 0;\n    // Number of iterations is proportional to the array length\n    for _ in nums {\n        count += 1;\n    }\n    count\n}\n
    time_complexity.c
    /* Linear order (traversing array) */\nint arrayTraversal(int *nums, int n) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.kt
    /* Linear order (traversing array) */\nfun arrayTraversal(nums: IntArray): Int {\n    var count = 0\n    // Number of iterations is proportional to the array length\n    for (num in nums) {\n        count++\n    }\n    return count\n}\n
    time_complexity.rb
    ### Linear time (array traversal) ###\ndef array_traversal(nums)\n  count = 0\n\n  # Number of iterations is proportional to the array length\n  for num in nums\n    count += 1\n  end\n\n  count\nend\n

    It is worth noting that the input data size \\(n\\) should be determined according to the type of input data. For example, in the first example, the variable \\(n\\) is the input data size; in the second example, the array length \\(n\\) is the data size.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-quadratic-order-on2","level":3,"title":"3.   Quadratic Order \\(O(n^2)\\)","text":"

    The number of operations in quadratic order grows quadratically relative to the input data size \\(n\\). Quadratic order typically appears in nested loops, where both the outer and inner loops have a time complexity of \\(O(n)\\), resulting in an overall time complexity of \\(O(n^2)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def quadratic(n: int) -> int:\n    \"\"\"Quadratic order\"\"\"\n    count = 0\n    # Number of iterations is quadratically related to the data size n\n    for i in range(n):\n        for j in range(n):\n            count += 1\n    return count\n
    time_complexity.cpp
    /* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nint Quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfunc quadratic(n int) int {\n    count := 0\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfunc quadratic(n: Int) -> Int {\n    var count = 0\n    // Number of iterations is quadratically related to the data size n\n    for _ in 0 ..< n {\n        for _ in 0 ..< n {\n            count += 1\n        }\n    }\n    return count\n}\n
    time_complexity.js
    /* Exponential order */\nfunction quadratic(n) {\n    let count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfunction quadratic(n: number): number {\n    let count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nint quadratic(int n) {\n  int count = 0;\n  // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfn quadratic(n: i32) -> i32 {\n    let mut count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for _ in 0..n {\n        for _ in 0..n {\n            count += 1;\n        }\n    }\n    count\n}\n
    time_complexity.c
    /* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfun quadratic(n: Int): Int {\n    var count = 0\n    // Number of iterations is quadratically related to the data size 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
    ### Quadratic time ###\ndef quadratic(n)\n  count = 0\n\n  # Number of iterations is quadratically related to the data size 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

    Figure 2-10 compares constant order, linear order, and quadratic order time complexities.

    Figure 2-10   Time complexities of constant, linear, and quadratic orders

    Taking bubble sort as an example, the outer loop executes \\(n - 1\\) times, and the inner loop executes \\(n-1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\) times, averaging \\(n / 2\\) times, resulting in a time complexity of \\(O((n - 1) n / 2) = O(n^2)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def bubble_sort(nums: list[int]) -> int:\n    \"\"\"Quadratic order (bubble sort)\"\"\"\n    count = 0  # Counter\n    # Outer loop: unsorted range is [0, i]\n    for i in range(len(nums) - 1, 0, -1):\n        # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                tmp: int = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3  # Element swap includes 3 unit operations\n    return count\n
    time_complexity.cpp
    /* Quadratic order (bubble sort) */\nint bubbleSort(vector<int> &nums) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.java
    /* Quadratic order (bubble sort) */\nint bubbleSort(int[] nums) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.cs
    /* Quadratic order (bubble sort) */\nint BubbleSort(int[] nums) {\n    int count = 0;  // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                count += 3;  // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.go
    /* Quadratic order (bubble sort) */\nfunc bubbleSort(nums []int) int {\n    count := 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                tmp := nums[j]\n                nums[j] = nums[j+1]\n                nums[j+1] = tmp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
    time_complexity.swift
    /* Quadratic order (bubble sort) */\nfunc bubbleSort(nums: inout [Int]) -> Int {\n    var count = 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
    time_complexity.js
    /* Quadratic order (bubble sort) */\nfunction bubbleSort(nums) {\n    let count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.ts
    /* Quadratic order (bubble sort) */\nfunction bubbleSort(nums: number[]): number {\n    let count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.dart
    /* Quadratic order (bubble sort) */\nint bubbleSort(List<int> nums) {\n  int count = 0; // Counter\n  // Outer loop: unsorted range is [0, i]\n  for (var i = nums.length - 1; i > 0; i--) {\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (var j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        count += 3; // Element swap includes 3 unit operations\n      }\n    }\n  }\n  return count;\n}\n
    time_complexity.rs
    /* Quadratic order (bubble sort) */\nfn bubble_sort(nums: &mut [i32]) -> i32 {\n    let mut count = 0; // Counter\n\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    count\n}\n
    time_complexity.c
    /* Quadratic order (bubble sort) */\nint bubbleSort(int *nums, int n) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = n - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.kt
    /* Quadratic order (bubble sort) */\nfun bubbleSort(nums: IntArray): Int {\n    var count = 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
    time_complexity.rb
    ### Quadratic time (bubble sort) ###\ndef bubble_sort(nums)\n  count = 0  # Counter\n\n  # Outer loop: unsorted range is [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Element swap includes 3 unit operations\n      end\n    end\n  end\n\n  count\nend\n
    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-exponential-order-o2n","level":3,"title":"4.   Exponential Order \\(O(2^n)\\)","text":"

    Biological \"cell division\" is a typical example of exponential order growth: the initial state is \\(1\\) cell, after one round of division it becomes \\(2\\), after two rounds it becomes \\(4\\), and so on; after \\(n\\) rounds of division there are \\(2^n\\) cells.

    Figure 2-11 and the following code simulate the cell division process, with a time complexity of \\(O(2^n)\\). Note that the input \\(n\\) represents the number of division rounds, and the return value count represents the total number of divisions.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def exponential(n: int) -> int:\n    \"\"\"Exponential order (loop implementation)\"\"\"\n    count = 0\n    base = 1\n    # Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint Exponential(int n) {\n    int count = 0, bas = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfunc exponential(n int) int {\n    count, base := 0, 1\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfunc exponential(n: Int) -> Int {\n    var count = 0\n    var base = 1\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfunction exponential(n) {\n    let count = 0,\n        base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfunction exponential(n: number): number {\n    let count = 0,\n        base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint exponential(int n) {\n  int count = 0, base = 1;\n  // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfn exponential(n: i32) -> i32 {\n    let mut count = 0;\n    let mut base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0;\n    int bas = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfun exponential(n: Int): Int {\n    var count = 0\n    var base = 1\n    // Cells divide into two every round, forming sequence 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
    ### Exponential time (iterative) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # Cells divide into two every round, forming sequence 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

    Figure 2-11   Time complexity of exponential order

    In actual algorithms, exponential order often appears in recursive functions. For example, in the following code, it recursively splits in two, stopping after \\(n\\) splits:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def exp_recur(n: int) -> int:\n    \"\"\"Exponential order (recursive implementation)\"\"\"\n    if n == 1:\n        return 1\n    return exp_recur(n - 1) + exp_recur(n - 1) + 1\n
    time_complexity.cpp
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\nint ExpRecur(int n) {\n    if (n == 1) return 1;\n    return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n
    time_complexity.go
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\nfunction expRecur(n) {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
    time_complexity.ts
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\nint expRecur(int n) {\n  if (n == 1) return 1;\n  return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
    time_complexity.rs
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\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
    ### Exponential time (recursive) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n

    Exponential order growth is very rapid and is common in exhaustive methods (brute force search, backtracking, etc.). For problems with large data scales, exponential order is unacceptable and typically requires dynamic programming or greedy algorithms to solve.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-logarithmic-order-olog-n","level":3,"title":"5.   Logarithmic Order \\(O(\\log n)\\)","text":"

    In contrast to exponential order, logarithmic order reflects the situation of \"reducing to half each round\". Let the input data size be \\(n\\). Since it is reduced to half each round, the number of loops is \\(\\log_2 n\\), which is the inverse function of \\(2^n\\).

    Figure 2-12 and the following code simulate the process of \"reducing to half each round\", with a time complexity of \\(O(\\log_2 n)\\), abbreviated as \\(O(\\log n)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def logarithmic(n: int) -> int:\n    \"\"\"Logarithmic order (loop implementation)\"\"\"\n    count = 0\n    while n > 1:\n        n = n / 2\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    ### Logarithmic time (iterative) ###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n

    Figure 2-12   Time complexity of logarithmic order

    Like exponential order, logarithmic order also commonly appears in recursive functions. The following code forms a recursion tree of height \\(\\log_2 n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def log_recur(n: int) -> int:\n    \"\"\"Logarithmic order (recursive implementation)\"\"\"\n    if n <= 1:\n        return 0\n    return log_recur(n / 2) + 1\n
    time_complexity.cpp
    /* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.java
    /* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.cs
    /* Logarithmic order (recursive implementation) */\nint LogRecur(int n) {\n    if (n <= 1) return 0;\n    return LogRecur(n / 2) + 1;\n}\n
    time_complexity.go
    /* Logarithmic order (recursive implementation) */\nfunc logRecur(n int) int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n/2) + 1\n}\n
    time_complexity.swift
    /* Logarithmic order (recursive implementation) */\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
    /* Logarithmic order (recursive implementation) */\nfunction logRecur(n) {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.ts
    /* Logarithmic order (recursive implementation) */\nfunction logRecur(n: number): number {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.dart
    /* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n  if (n <= 1) return 0;\n  return logRecur(n ~/ 2) + 1;\n}\n
    time_complexity.rs
    /* Logarithmic order (recursive implementation) */\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
    /* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.kt
    /* Logarithmic order (recursive implementation) */\nfun logRecur(n: Int): Int {\n    if (n <= 1)\n        return 0\n    return logRecur(n / 2) + 1\n}\n
    time_complexity.rb
    ### Logarithmic time (recursive) ###\ndef log_recur(n)\n  return 0 unless n > 1\n  log_recur(n / 2) + 1\nend\n

    Logarithmic order commonly appears in algorithms based on the divide-and-conquer strategy, reflecting the idea of repeatedly splitting a problem and simplifying it. It grows slowly and is the ideal time complexity second only to constant order.

    What is the base of \\(O(\\log n)\\)?

    To be precise, \"dividing into \\(m\\)\" corresponds to a time complexity of \\(O(\\log_m n)\\). And through the logarithmic base change formula, we can obtain time complexities with different bases that are equal:

    \\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\]

    That is to say, the base \\(m\\) can be converted without affecting the complexity. Therefore, we usually omit the base \\(m\\) and denote logarithmic order simply as \\(O(\\log n)\\).

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-linearithmic-order-on-log-n","level":3,"title":"6.   Linearithmic Order \\(O(n \\log n)\\)","text":"

    Linearithmic order commonly appears in nested loops, where the time complexities of the two layers of loops are \\(O(\\log n)\\) and \\(O(n)\\) respectively. The relevant code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def linear_log_recur(n: int) -> int:\n    \"\"\"Linearithmic order\"\"\"\n    if n <= 1:\n        return 1\n    # Divide into two, the scale of subproblems is reduced by half\n    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n    # Current subproblem contains n operations\n    for _ in range(n):\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    ### Linearithmic time ###\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

    Figure 2-13 shows how linearithmic order is generated. Each level of the binary tree has a total of \\(n\\) operations, and the tree has \\(\\log_2 n + 1\\) levels, resulting in a time complexity of \\(O(n \\log n)\\).

    Figure 2-13   Time complexity of linearithmic order

    Mainstream sorting algorithms typically have a time complexity of \\(O(n \\log n)\\), such as quicksort, merge sort, and heap sort.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-factorial-order-on","level":3,"title":"7.   Factorial Order \\(O(n!)\\)","text":"

    Factorial order corresponds to the mathematical \"permutation\" problem. Given \\(n\\) distinct elements, find all possible permutation schemes; the number of schemes is:

    \\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\]

    Factorials are typically implemented using recursion. As shown in Figure 2-14 and the following code, the first level splits into \\(n\\) branches, the second level splits into \\(n - 1\\) branches, and so on, until the \\(n\\)-th level when splitting stops:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def factorial_recur(n: int) -> int:\n    \"\"\"Factorial order (recursive implementation)\"\"\"\n    if n == 0:\n        return 1\n    count = 0\n    # Split from 1 into n\n    for _ in range(n):\n        count += factorial_recur(n - 1)\n    return count\n
    time_complexity.cpp
    /* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.java
    /* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.cs
    /* Factorial order (recursive implementation) */\nint FactorialRecur(int n) {\n    if (n == 0) return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += FactorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.go
    /* Factorial order (recursive implementation) */\nfunc factorialRecur(n int) int {\n    if n == 0 {\n        return 1\n    }\n    count := 0\n    // Split from 1 into n\n    for i := 0; i < n; i++ {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
    time_complexity.swift
    /* Factorial order (recursive implementation) */\nfunc factorialRecur(n: Int) -> Int {\n    if n == 0 {\n        return 1\n    }\n    var count = 0\n    // Split from 1 into n\n    for _ in 0 ..< n {\n        count += factorialRecur(n: n - 1)\n    }\n    return count\n}\n
    time_complexity.js
    /* Factorial order (recursive implementation) */\nfunction factorialRecur(n) {\n    if (n === 0) return 1;\n    let count = 0;\n    // Split from 1 into n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.ts
    /* Factorial order (recursive implementation) */\nfunction factorialRecur(n: number): number {\n    if (n === 0) return 1;\n    let count = 0;\n    // Split from 1 into n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.dart
    /* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n  if (n == 0) return 1;\n  int count = 0;\n  // Split from 1 into n\n  for (var i = 0; i < n; i++) {\n    count += factorialRecur(n - 1);\n  }\n  return count;\n}\n
    time_complexity.rs
    /* Factorial order (recursive implementation) */\nfn factorial_recur(n: i32) -> i32 {\n    if n == 0 {\n        return 1;\n    }\n    let mut count = 0;\n    // Split from 1 into n\n    for _ in 0..n {\n        count += factorial_recur(n - 1);\n    }\n    count\n}\n
    time_complexity.c
    /* Factorial order (recursive implementation) */\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
    /* Factorial order (recursive implementation) */\nfun factorialRecur(n: Int): Int {\n    if (n == 0)\n        return 1\n    var count = 0\n    // Split from 1 into n\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
    time_complexity.rb
    ### Factorial time (recursive) ###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # Split from 1 into n\n  (0...n).each { count += factorial_recur(n - 1) }\n\n  count\nend\n

    Figure 2-14   Time complexity of factorial order

    Note that because when \\(n \\geq 4\\) we always have \\(n! > 2^n\\), factorial order grows faster than exponential order, and is also unacceptable for large \\(n\\).

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235-worst-best-and-average-time-complexities","level":2,"title":"2.3.5   Worst, Best, and Average Time Complexities","text":"

    The time efficiency of an algorithm is often not fixed, but is related to the distribution of the input data. Suppose we input an array nums of length \\(n\\), where nums consists of numbers from \\(1\\) to \\(n\\), with each number appearing only once, but the element order is randomly shuffled. The task is to return the index of element \\(1\\). We can draw the following conclusions.

    • When nums = [?, ?, ..., 1], i.e., when the last element is \\(1\\), it requires a complete traversal of the array, reaching worst-case time complexity \\(O(n)\\).
    • When nums = [1, ?, ?, ...], i.e., when the first element is \\(1\\), no matter how long the array is, there is no need to continue traversing, reaching best-case time complexity \\(\\Omega(1)\\).

    The \"worst-case time complexity\" corresponds to the function's asymptotic upper bound, denoted using big-\\(O\\) notation. Correspondingly, the \"best-case time complexity\" corresponds to the function's asymptotic lower bound, denoted using \\(\\Omega\\) notation:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py
    def random_numbers(n: int) -> list[int]:\n    \"\"\"Generate an array with elements: 1, 2, ..., n, shuffled in order\"\"\"\n    # Generate array nums =: 1, 2, 3, ..., n\n    nums = [i for i in range(1, n + 1)]\n    # Randomly shuffle array elements\n    random.shuffle(nums)\n    return nums\n\ndef find_one(nums: list[int]) -> int:\n    \"\"\"Find the index of number 1 in array nums\"\"\"\n    for i in range(len(nums)):\n        # When element 1 is at the head of the array, best time complexity O(1) is achieved\n        # When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1:\n            return i\n    return -1\n
    worst_best_time_complexity.cpp
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nvector<int> randomNumbers(int n) {\n    vector<int> nums(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Use system time to generate random seed\n    unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n    // Randomly shuffle array elements\n    shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(vector<int> &nums) {\n    for (int i = 0; i < nums.size(); i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
    worst_best_time_complexity.java
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint[] randomNumbers(int n) {\n    Integer[] nums = new Integer[n];\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nint findOne(int[] nums) {\n    for (int i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
    worst_best_time_complexity.cs
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint[] RandomNumbers(int n) {\n    int[] nums = new int[n];\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nint FindOne(int[] nums) {\n    for (int i = 0; i < nums.Length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
    worst_best_time_complexity.go
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunc randomNumbers(n int) []int {\n    nums := make([]int, n)\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for i := 0; i < n; i++ {\n        nums[i] = i + 1\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nfunc findOne(nums []int) int {\n    for i := 0; i < len(nums); i++ {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
    worst_best_time_complexity.swift
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunc randomNumbers(n: Int) -> [Int] {\n    // Generate array nums = { 1, 2, 3, ..., n }\n    var nums = Array(1 ... n)\n    // Randomly shuffle array elements\n    nums.shuffle()\n    return nums\n}\n\n/* Find the index of number 1 in array nums */\nfunc findOne(nums: [Int]) -> Int {\n    for i in nums.indices {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
    worst_best_time_complexity.js
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunction randomNumbers(n) {\n    const nums = Array(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nfunction findOne(nums) {\n    for (let i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
    worst_best_time_complexity.ts
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunction randomNumbers(n: number): number[] {\n    const nums = Array(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nfunction findOne(nums: number[]): number {\n    for (let i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
    worst_best_time_complexity.dart
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nList<int> randomNumbers(int n) {\n  final nums = List.filled(n, 0);\n  // Generate array nums = { 1, 2, 3, ..., n }\n  for (var i = 0; i < n; i++) {\n    nums[i] = i + 1;\n  }\n  // Randomly shuffle array elements\n  nums.shuffle();\n\n  return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(List<int> nums) {\n  for (var i = 0; i < nums.length; i++) {\n    // When element 1 is at the head of the array, best time complexity O(1) is achieved\n    // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n    if (nums[i] == 1) return i;\n  }\n\n  return -1;\n}\n
    worst_best_time_complexity.rs
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfn random_numbers(n: i32) -> Vec<i32> {\n    // Generate array nums = { 1, 2, 3, ..., n }\n    let mut nums = (1..=n).collect::<Vec<i32>>();\n    // Randomly shuffle array elements\n    nums.shuffle(&mut thread_rng());\n    nums\n}\n\n/* Find the index of number 1 in array nums */\nfn find_one(nums: &[i32]) -> Option<usize> {\n    for i in 0..nums.len() {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return Some(i);\n        }\n    }\n    None\n}\n
    worst_best_time_complexity.c
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint *randomNumbers(int n) {\n    // Allocate heap memory (create 1D variable-length array: n elements of type int)\n    int *nums = (int *)malloc(n * sizeof(int));\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nint findOne(int *nums, int n) {\n    for (int i = 0; i < n; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
    worst_best_time_complexity.kt
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfun randomNumbers(n: Int): Array<Int?> {\n    val nums = IntArray(n)\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (i in 0..<n) {\n        nums[i] = i + 1\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nfun findOne(nums: Array<Int?>): Int {\n    for (i in nums.indices) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i\n    }\n    return -1\n}\n
    worst_best_time_complexity.rb
    ### Generate array with elements: 1, 2, ..., n, shuffled ###\ndef random_numbers(n)\n  # Generate array nums =: 1, 2, 3, ..., n\n  nums = Array.new(n) { |i| i + 1 }\n  # Randomly shuffle array elements\n  nums.shuffle!\nend\n\n### Find index of number 1 in array nums ###\ndef find_one(nums)\n  for i in 0...nums.length\n    # When element 1 is at the head of the array, best time complexity O(1) is achieved\n    # When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n    return i if nums[i] == 1\n  end\n\n  -1\nend\n

    It is worth noting that we rarely use best-case time complexity in practice, because it can usually only be achieved with a very small probability and may be somewhat misleading. The worst-case time complexity is more practical because it gives a safety value for efficiency, allowing us to use the algorithm with confidence.

    From the above example, we can see that both worst-case and best-case time complexities arise only under particular input distributions, which may occur with very low probability and may not truly reflect the algorithm's running efficiency. In contrast, average time complexity can reflect the algorithm's running efficiency under random input data, denoted using the \\(\\Theta\\) notation.

    For some algorithms, we can simply derive the average case under random data distribution. For example, in the above example, since the input array is shuffled, the probability of element \\(1\\) appearing at any index is equal, so the algorithm's average number of loops is half the array length \\(n / 2\\), giving an average time complexity of \\(\\Theta(n / 2) = \\Theta(n)\\).

    But for more complex algorithms, calculating average time complexity is often quite difficult, because it is hard to analyze the overall mathematical expectation under data distribution. In this case, we usually use worst-case time complexity as the criterion for judging algorithm efficiency.

    Why is the \\(\\Theta\\) symbol rarely seen?

    This may be because the \\(O\\) symbol is too catchy, so we often use it to represent average time complexity. But strictly speaking, this practice is not standard. In this book and other materials, if you encounter expressions like \"average time complexity \\(O(n)\\)\", please understand it directly as \\(\\Theta(n)\\).

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"Chapter 3.   Data Structures","text":"

    Abstract

    Data structures are like a sturdy and diverse framework.

    It provides a blueprint for the orderly organization of data, upon which algorithms come to life.

    ","path":["Chapter 3. Data Structures","Chapter 3.   Data Structures"],"tags":[]},{"location":"chapter_data_structure/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 3.1   Classification of Data Structures
    • 3.2   Basic Data Types
    • 3.3   Number Encoding *
    • 3.4   Character Encoding *
    • 3.5   Summary
    ","path":["Chapter 3. Data Structures","Chapter 3.   Data Structures"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   Basic Data Types","text":"

    When we talk about data stored in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these kinds of data are organized in different ways, they are all composed of various basic data types.

    Basic data types are types that the CPU can directly operate on, and they are directly used in algorithms, mainly including the following.

    • Integer types byte, short, int, long.
    • Floating-point types float, double, used to represent decimal numbers.
    • Character type char, used to represent letters, punctuation marks, and even emojis in various languages.
    • Boolean type bool, used to represent \"yes\" and \"no\" judgments.

    Basic data types are stored in binary form in computers. A binary digit is one bit. In most modern operating systems, \\(1\\) byte consists of \\(8\\) bits.

    The range of values for basic data types depends on the size of the space they occupy. Below is an example using Java.

    • Integer type byte occupies \\(1\\) byte = \\(8\\) bits, and can represent \\(2^{8}\\) numbers.
    • Integer type int occupies \\(4\\) bytes = \\(32\\) bits, and can represent \\(2^{32}\\) numbers.

    The following table lists the space occupied, value ranges, and default values of various basic data types in Java. You don't need to memorize this table; a general understanding is sufficient, and you can refer to it when needed.

    Table 3-1   Space occupied and value ranges of basic data types

    Type Symbol Space Occupied Minimum Value Maximum Value Default Value Integer byte 1 byte \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) short 2 bytes \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) int 4 bytes \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) long 8 bytes \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) Float float 4 bytes \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) double 8 bytes \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) Character char 2 bytes \\(0\\) \\(2^{16} - 1\\) \\(0\\) Boolean bool 1 byte \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\)

    Please note that Table 3-1 applies specifically to Java's basic data types. Each programming language has its own type definitions, and their space usage, value ranges, and default values may vary.

    • In Python, the integer type int can be of any size, limited only by available memory; the floating-point type float is double-precision 64-bit; there is no char type, a single character is actually a string str of length 1.
    • C and C++ do not explicitly specify the size of basic data types, which varies by implementation and platform. The above table follows the LP64 data model, which is used in Unix 64-bit operating systems including Linux and macOS.
    • The size of character char is 1 byte in C and C++, and in most programming languages it depends on the specific character encoding method, as detailed in the \"Character Encoding\" section.
    • Even though representing a boolean value requires only 1 bit (\\(0\\) or \\(1\\)), it is usually stored as 1 byte in memory. This is because modern computer CPUs typically use 1 byte as the minimum addressable memory unit.

    So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. Here, the emphasis is on the \"structure\", not the \"data\".

    If we want to represent \"a row of numbers\", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but whether the stored content is integer int, floating-point float, or character char is unrelated to the \"data structure\".

    In other words, basic data types provide the \"content type\" of data, while data structures provide the \"organization method\" of data. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including int, float, char, bool, etc.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # Initialize arrays using various basic data types\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# In Python, characters are actually strings of length 1\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Python lists can freely store various basic data types and object references\ndata = [0, 0.0, 'a', False, ListNode(0)]\n
    // Initialize arrays using various basic data types\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n
    // Initialize arrays using various basic data types\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n
    // Initialize arrays using various basic data types\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n
    // Initialize arrays using various basic data types\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n
    // Initialize arrays using various basic data types\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 arrays can freely store various basic data types and objects\nconst array = [0, 0.0, 'a', false];\n
    // Initialize arrays using various basic data types\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n
    // Initialize arrays using various basic data types\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
    // Initialize arrays using various basic data types\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
    // Initialize arrays using various basic data types\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n
    // Initialize arrays using various basic data types\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n
    # Ruby lists can freely store various basic data types and object references\ndata = [0, 0.0, 'a', false, ListNode(0)]\n
    Visualized Execution

    https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

    ","path":["Chapter 3. Data Structures","3.2   Basic Data Types"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   Character Encoding *","text":"

    In computers, all data is stored in binary form, and character char is no exception. To represent characters, we need to establish a \"character set\" that defines a one-to-one correspondence between each character and binary numbers. With a character set, computers can convert binary numbers to characters by looking up the table.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii-character-set","level":2,"title":"3.4.1   ASCII Character Set","text":"

    ASCII code is the earliest character set, with the full name American Standard Code for Information Interchange. It uses 7 binary bits (the lower 7 bits of one byte) to represent a character, and can represent a maximum of 128 different characters. As shown in Figure 3-6, ASCII code includes uppercase and lowercase English letters, numbers 0 ~ 9, some punctuation marks, and some control characters (such as newline and tab).

    Figure 3-6   ASCII code

    However, ASCII code can only represent English. With the globalization of computers, a character set called EASCII that can represent more languages emerged. It expands from the 7-bit basis of ASCII to 8 bits, and can represent 256 different characters.

    Worldwide, a batch of EASCII character sets suitable for different regions have appeared successively. The first 128 characters of these character sets are unified as ASCII code, and the last 128 characters are defined differently to adapt to the needs of different languages.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk-character-set","level":2,"title":"3.4.2   GBK Character Set","text":"

    Later, people found that EASCII still could not provide enough characters for many languages. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used in everyday life. In 1980, the China National Standardization Administration released the GB2312 character set, which included 6,763 Chinese characters, basically meeting the needs of computer processing for Chinese.

    However, GB2312 cannot handle some rare characters and traditional Chinese characters. The GBK character set is an extension based on GB2312, which includes a total of 21,886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented using one byte, and Chinese characters are represented using two bytes.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode-character-set","level":2,"title":"3.4.3   Unicode Character Set","text":"

    With the vigorous development of computer technology, character sets and encoding standards flourished, which brought many problems. On the one hand, these character sets generally only define characters for specific languages and cannot work normally in multilingual environments. On the other hand, multiple character set standards exist for the same language, and if two computers use different encoding standards, garbled characters will appear during information transmission.

    Researchers of that era thought: If a sufficiently complete character set were released to include all languages and symbols in the world, wouldn't that solve problems in cross-language environments and eliminate garbled text? Driven by this idea, a large and comprehensive character set, Unicode, was born.

    Unicode, or Unified Code, can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards.

    Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. In practical storage and encoding schemes for this vast character set, commonly used characters often occupy 2 bytes, while some rare characters occupy 3 bytes or even 4 bytes.

    Unicode is a universal character set that essentially assigns a number (called a \"code point\") to each character, but it does not specify how to store these character code points in computers. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters?

    For the above problem, a straightforward solution is to store all characters as equal-length encodings. As shown in Figure 3-7, each character in \"Hello\" occupies 1 byte, and each character in \"算法\" (algorithm) occupies 2 bytes. We can encode all characters in \"Hello 算法\" as 2 bytes in length by padding the high bits with 0. In this way, the system can parse one character every 2 bytes and restore the content of this phrase.

    Figure 3-7   Unicode encoding example

    However, ASCII code has already proven to us that encoding English only requires 1 byte. If the above scheme is adopted, the size of English text will be twice that under ASCII encoding, which is very wasteful of memory space. Therefore, we need a more efficient Unicode encoding method.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8-encoding","level":2,"title":"3.4.4   UTF-8 Encoding","text":"

    Currently, UTF-8 has become the most widely used Unicode encoding method internationally. It is a variable-length encoding that uses 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters only require 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters require 3 bytes, and some other rare characters require 4 bytes.

    The encoding rules of UTF-8 are not complicated and can be divided into the following two cases.

    • For 1-byte characters, set the highest bit to \\(0\\), and set the remaining 7 bits to the Unicode code point. It is worth noting that ASCII characters occupy the first 128 code points in the Unicode character set. That is to say, UTF-8 encoding is backward compatible with ASCII code. This means we can use UTF-8 to parse very old ASCII code text.
    • For characters with a length of \\(n\\) bytes (where \\(n > 1\\)), set the highest \\(n\\) bits of the first byte to \\(1\\), and set the \\((n + 1)\\)-th bit to \\(0\\); starting from the second byte, set the highest 2 bits of each byte to \\(10\\); use all remaining bits to fill in the Unicode code point of the character.

    Figure 3-8 shows the UTF-8 encoding corresponding to \"Hello 算法\". It can be observed that since the highest \\(n\\) bits are all set to \\(1\\), the system can determine that the character length is \\(n\\) by counting the leading \\(1\\) bits.

    But why set the highest 2 bits of all other bytes to \\(10\\)? In fact, this \\(10\\) can serve as a check symbol. Assuming the system starts parsing text from an incorrect byte, the \\(10\\) at the beginning of the byte can help the system quickly determine an anomaly.

    The reason for using \\(10\\) as a check symbol is that under UTF-8 encoding rules, it is impossible for a character's highest two bits to be \\(10\\). This conclusion can be proven by contradiction: assuming the highest two bits of a character are \\(10\\), it means the length of the character is \\(1\\), corresponding to ASCII code. However, the highest bit of ASCII code should be \\(0\\), which contradicts the assumption.

    Figure 3-8   UTF-8 encoding example

    In addition to UTF-8, common encoding methods also include the following two.

    • UTF-16 encoding: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters need to use 4 bytes. For 2-byte characters, UTF-16 encoding is equal to the Unicode code point.
    • UTF-32 encoding: Every character uses 4 bytes. This means that UTF-32 takes up more space than UTF-8 and UTF-16, especially for text with a high proportion of ASCII characters.

    From the perspective of storage space occupation, using UTF-8 to represent English characters is very efficient because it only requires 1 byte; using UTF-16 encoding for some non-English characters (such as Chinese) will be more efficient because it only requires 2 bytes, while UTF-8 may require 3 bytes.

    From a compatibility perspective, UTF-8 has the best universality, and many tools and libraries support UTF-8 first.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345-character-encoding-in-programming-languages","level":2,"title":"3.4.5   Character Encoding in Programming Languages","text":"

    For many programming languages in the past, strings during program execution used internal encodings such as UTF-16 or UTF-32. Under these representations, we can often treat strings like arrays during processing, and this approach has the following advantages.

    • Random access: UTF-16 encoded strings can be easily accessed randomly. UTF-8 is a variable-length encoding. To find the \\(i\\)-th character, we need to traverse from the beginning of the string to the \\(i\\)-th character, which requires \\(O(n)\\) time.
    • Character counting: Similar to random access, calculating the length of a UTF-16 encoded string is also an \\(O(1)\\) operation. However, calculating the length of a UTF-8 encoded string requires traversing the entire string.
    • String operations: Many string operations (such as splitting, joining, inserting, deleting, etc.) on UTF-16 encoded strings are easier to perform. Performing these operations on UTF-8 encoded strings usually requires additional calculations to ensure that invalid UTF-8 encoding is not generated.

    In fact, the design of character encoding schemes for programming languages is a very interesting topic involving many factors.

    • Java's String type uses UTF-16 encoding, with each character occupying 2 bytes. This is because at the beginning of Java language design, people believed that 16 bits were sufficient to represent all possible characters. However, this was an incorrect judgment. Later, the Unicode specification expanded beyond 16 bits, so characters in Java may now be represented by a pair of 16-bit values (called \"surrogate pairs\").
    • The strings of JavaScript and TypeScript use UTF-16 encoding for reasons similar to Java. When Netscape first introduced the JavaScript language in 1995, Unicode was still in its early stages of development, and at that time, using 16-bit encoding was sufficient to represent all Unicode characters.
    • C# uses UTF-16 encoding mainly because the .NET platform was designed by Microsoft, and many of Microsoft's technologies (including the Windows operating system) extensively use UTF-16 encoding.

    Due to the underestimation of character quantities by the above programming languages, they had to adopt the \"surrogate pair\" method to represent Unicode characters with lengths exceeding 16 bits. This is a reluctant compromise. On the one hand, in strings containing surrogate pairs, one character may occupy 2 bytes or 4 bytes, thus losing the advantage of fixed-length encoding. On the other hand, handling surrogate pairs requires additional code, which increases the complexity and difficulty of debugging in programming.

    For the above reasons, some programming languages have proposed different encoding schemes.

    • Python's str uses Unicode encoding and adopts a flexible string representation where the stored character length depends on the largest Unicode code point in the string. If all characters in the string are ASCII characters, each character occupies 1 byte; if there are characters exceeding the ASCII range but all within the Basic Multilingual Plane (BMP), each character occupies 2 bytes; if there are characters exceeding the BMP, each character occupies 4 bytes.
    • Go language's string type uses UTF-8 encoding internally. Go language also provides the rune type, which is used to represent a single Unicode code point.
    • Rust language's str and String types use UTF-8 encoding internally. Rust also provides the char type for representing a single Unicode code point.

    It should be noted that the above discussion is about how strings are stored in programming languages, which is different from how strings are stored in files or transmitted over networks. In file storage or network transmission, we usually encode strings into UTF-8 format to achieve optimal compatibility and space efficiency.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   Classification of Data Structures","text":"

    Common data structures include arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. They can be classified from two dimensions: \"logical structure\" and \"physical structure\".

    ","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311-logical-structure-linear-and-non-linear","level":2,"title":"3.1.1   Logical Structure: Linear and Non-Linear","text":"

    Logical structure reveals the logical relationships between data elements. In arrays and linked lists, data is arranged in a certain order, embodying linear relationships between elements; while in trees, data is arranged hierarchically from top to bottom, showing parent-descendant relationships; graphs are composed of nodes and edges, reflecting complex network relationships.

    As shown in Figure 3-1, logical structures can be divided into two major categories: \"linear\" and \"non-linear\". Linear structures are more intuitive, indicating that data is linearly arranged in logical relationships; non-linear structures are the opposite, arranged non-linearly.

    • Linear data structures: Arrays, linked lists, stacks, queues, hash tables, where elements have a one-to-one sequential relationship.
    • Non-linear data structures: Trees, heaps, graphs, hash tables.

    Non-linear data structures can be further divided into tree structures and network structures.

    • Tree structures: Trees, heaps, hash tables, where elements have a one-to-many relationship.
    • Network structures: Graphs, where elements have a many-to-many relationship.

    Figure 3-1   Linear and non-linear data structures

    ","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312-physical-structure-contiguous-and-dispersed","level":2,"title":"3.1.2   Physical Structure: Contiguous and Dispersed","text":"

    When an algorithm program runs, the data being processed is mainly stored in memory. Figure 3-2 shows a computer memory stick, where each black square contains a memory space. We can imagine memory as a huge Excel spreadsheet, where each cell can store a certain amount of data.

    The system accesses data at the target location through memory addresses. As shown in Figure 3-2, the computer assigns a number to each cell in the spreadsheet according to specific rules, ensuring that each memory space has a unique memory address. With these addresses, the program can access data in memory.

    Figure 3-2   Memory stick, memory space, memory address

    Tip

    It should be noted that comparing memory to an Excel spreadsheet is only a simplified analogy. The actual workings of memory are much more complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory.

    Memory is a shared resource for all programs. When a block of memory is occupied by a program, it usually cannot be used by other programs at the same time. Therefore, in the design of data structures and algorithms, memory resources are an important consideration. For example, the peak memory occupied by an algorithm should not exceed the remaining free memory of the system; if there is a lack of contiguous large memory blocks, then the data structure chosen must be able to be stored in dispersed memory spaces.

    As shown in Figure 3-3, physical structure reflects the way data is stored in computer memory. It can be divided into contiguous-space storage (arrays) and dispersed-space storage (linked lists). At a low level, physical structure determines how data is accessed, updated, inserted, and deleted. These two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency.

    Figure 3-3   Contiguous space storage and dispersed space storage

    It is worth noting that all data structures are implemented based on arrays, linked lists, or a combination of both. For example, stacks and queues can be implemented using either arrays or linked lists; while the implementation of hash tables may include both arrays and linked lists.

    • Can be implemented based on arrays: Stacks, queues, hash tables, trees, heaps, graphs, matrices, tensors (arrays with dimensions \\(\\geq 3\\)), etc.
    • Can be implemented based on linked lists: Stacks, queues, hash tables, trees, heaps, graphs, etc.

    After initialization, linked lists can still adjust their length during program execution, so they are also called \"dynamic data structures\". After initialization, the length of arrays cannot be changed, so they are also called \"static data structures\". It is worth noting that arrays can change length by reallocating memory, thus retaining a limited degree of flexibility.

    Tip

    If you find it difficult to understand physical structure, it is recommended to read the next chapter first, and then review this section.

    ","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   Number Encoding *","text":"

    Tip

    In this book, chapters marked with an asterisk * are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters.

    ","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331-sign-magnitude-1s-complement-and-2s-complement","level":2,"title":"3.3.1   Sign-Magnitude, 1's Complement, and 2's Complement","text":"

    In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the byte range is \\([-128, 127]\\). This phenomenon is counterintuitive, and its underlying cause lies in sign-magnitude, 1's complement, and 2's complement representations.

    First, it should be noted that numbers are stored in computers in the form of \"2's complement\". Before analyzing the reasons for this, let's first define these three concepts.

    • Sign-magnitude: We treat the highest bit of the binary representation of a number as the sign bit, where \\(0\\) represents a positive number and \\(1\\) represents a negative number, and the remaining bits represent the value of the number.
    • 1's complement: The 1's complement of a positive number is the same as its sign-magnitude. For a negative number, the 1's complement is obtained by inverting all bits except the sign bit of its sign-magnitude.
    • 2's complement: The 2's complement of a positive number is the same as its sign-magnitude. For a negative number, the 2's complement is obtained by adding \\(1\\) to its 1's complement.

    Figure 3-4 shows the conversion methods among sign-magnitude, 1's complement, and 2's complement.

    Figure 3-4   Conversions among sign-magnitude, 1's complement, and 2's complement

    Sign-magnitude, although the most intuitive, has some limitations. On one hand, the sign-magnitude of negative numbers cannot be directly used in operations. For example, calculating \\(1 + (-2)\\) in sign-magnitude yields \\(-3\\), which is clearly incorrect.

    \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

    To solve this problem, computers introduced 1's complement. If we first convert sign-magnitude to 1's complement and calculate \\(1 + (-2)\\) in 1's complement, then convert the result back to sign-magnitude, we can obtain the correct result of \\(-1\\).

    \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(Sign-magnitude)} + 1000 \\; 0010 \\; \\text{(Sign-magnitude)} \\newline & = 0000 \\; 0001 \\; \\text{(1's complement)} + 1111 \\; 1101 \\; \\text{(1's complement)} \\newline & = 1111 \\; 1110 \\; \\text{(1's complement)} \\newline & = 1000 \\; 0001 \\; \\text{(Sign-magnitude)} \\newline & \\rightarrow -1 \\end{aligned} \\]

    On the other hand, the sign-magnitude of the number zero has two representations, \\(+0\\) and \\(-0\\). This means that the number zero corresponds to two different binary encodings, which may cause ambiguity. For example, in conditional judgments, if we don't distinguish between positive zero and negative zero, it may lead to incorrect judgment results. If we want to handle the ambiguity of positive and negative zero, we need to introduce additional judgment operations, which may reduce the computational efficiency of the computer.

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

    Like sign-magnitude, 1's complement also has the problem of positive and negative zero ambiguity. Therefore, computers further introduced 2's complement. Let's first observe the conversion process of negative zero from sign-magnitude to 1's complement to 2's complement:

    \\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(Sign-magnitude)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(1's complement)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(2's complement)} \\newline \\end{aligned} \\]

    Adding \\(1\\) to the 1's complement of negative zero produces a carry, but since the byte type has a length of only 8 bits, the \\(1\\) that overflows to the 9th bit is discarded. That is to say, the 2's complement of negative zero is \\(0000 \\; 0000\\), which is the same as the 2's complement of positive zero. This means that in 2's complement representation, there is only one zero, and the positive and negative zero ambiguity is thus resolved.

    One last question remains: the range of the byte type is \\([-128, 127]\\), so where does the extra negative number \\(-128\\) come from? We notice that all integers in the interval \\([-127, +127]\\) have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other.

    However, the 2's complement \\(1000 \\; 0000\\) is an exception, and it does not have a corresponding sign-magnitude. According to the conversion method, we get that the sign-magnitude of this 2's complement is \\(0000 \\; 0000\\). This is clearly contradictory because this sign-magnitude represents the number \\(0\\), and its 2's complement should be itself. The computer specifies that this special 2's complement \\(1000 \\; 0000\\) represents \\(-128\\). In fact, the result of calculating \\((-1) + (-127)\\) in 2's complement is \\(-128\\).

    \\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(Sign-magnitude)} + 1000 \\; 0001 \\; \\text{(Sign-magnitude)} \\newline & = 1000 \\; 0000 \\; \\text{(1's complement)} + 1111 \\; 1110 \\; \\text{(1's complement)} \\newline & = 1000 \\; 0001 \\; \\text{(2's complement)} + 1111 \\; 1111 \\; \\text{(2's complement)} \\newline & = 1000 \\; 0000 \\; \\text{(2's complement)} \\newline & \\rightarrow -128 \\end{aligned} \\]

    You may have noticed that all the above calculations are addition operations. This hints at an important fact: the hardware circuits inside computers are mainly designed based on addition operations. This is because addition operations are simpler to implement in hardware compared to other operations (such as multiplication, division, and subtraction), easier to parallelize, and have faster operation speeds.

    Please note that this does not mean that computers can only perform addition. By combining addition with some basic logical operations, computers can implement various other mathematical operations. For example, calculating the subtraction \\(a - b\\) can be converted to calculating the addition \\(a + (-b)\\); calculating multiplication and division can be converted to calculating multiple additions or subtractions.

    We can now summarize why computers use 2's complement: with 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without designing special hardware circuits for subtraction or separately handling the ambiguity of positive and negative zero. This greatly simplifies hardware design and improves efficiency.

    The design of 2's complement is very ingenious. Due to space limitations, we will stop here. Interested readers are encouraged to explore further.

    ","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332-floating-point-number-encoding","level":2,"title":"3.3.2   Floating-Point Number Encoding","text":"

    Careful readers may have noticed: int and float have the same length, both are 4 bytes, but why does float have a much larger range than int? This is very counterintuitive because it stands to reason that float needs to represent decimals, so the range should be smaller.

    In fact, this is because floating-point number float uses a different representation method. Let's denote a 32-bit binary number as:

    \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

    According to the IEEE 754 standard, a 32-bit float consists of the following three parts.

    • Sign bit \\(\\mathrm{S}\\): occupies 1 bit, corresponding to \\(b_{31}\\).
    • Exponent bit \\(\\mathrm{E}\\): occupies 8 bits, corresponding to \\(b_{30} b_{29} \\ldots b_{23}\\).
    • Fraction bit \\(\\mathrm{N}\\): occupies 23 bits, corresponding to \\(b_{22} b_{21} \\ldots b_0\\).

    The calculation method for the value corresponding to the binary float is:

    \\[ \\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 \\]

    Converted to decimal, the calculation formula is:

    \\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\]

    The range of each component is:

    \\[ \\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} \\]

    Figure 3-5   Calculation example of float under IEEE 754 standard

    Observing Figure 3-5, given example data \\(\\mathrm{S} = 0\\), \\(\\mathrm{E} = 124\\), \\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\), we have:

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

    Now we can answer the initial question: the representation of float includes an exponent bit, resulting in a range far greater than int. According to the above calculation, the maximum positive number that float can represent is \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\), and the minimum negative number can be obtained by switching the sign bit.

    Although floating-point number float expands the range, its side effect is sacrificing precision. The integer type int uses all 32 bits to represent numbers, and the numbers are evenly distributed; however, due to the existence of the exponent bit, the larger the value of floating-point number float, the larger the difference between two adjacent numbers tends to be.

    As shown in Table 3-2, exponent bits \\(\\mathrm{E} = 0\\) and \\(\\mathrm{E} = 255\\) have special meanings, used to represent zero, infinity, \\(\\mathrm{NaN}\\), etc.

    Table 3-2   Meaning of exponent bits

    Exponent Bit E Fraction Bit \\(\\mathrm{N} = 0\\) Fraction Bit \\(\\mathrm{N} \\ne 0\\) Calculation Formula \\(0\\) \\(\\pm 0\\) Subnormal Number \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) Normal Number Normal Number \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\)

    It is worth noting that subnormal numbers significantly improve the precision of floating-point numbers. The smallest positive normal number is \\(2^{-126}\\), and the smallest positive subnormal number is \\(2^{-126} \\times 2^{-23}\\).

    Double-precision double also uses a representation method similar to float, which will not be elaborated here.

    ","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   Summary","text":"","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_data_structure/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Data structures can be classified from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory.
    • Common logical structures include linear, tree-like, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures.
    • When a program runs, data is stored in computer memory. Each memory space has a corresponding memory address, and the program accesses data through these memory addresses.
    • Physical structures are primarily divided into contiguous space storage (arrays) and dispersed space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both.
    • Basic data types in computers include integers byte, short, int, long, floating-point numbers float, double, characters char, and booleans bool. Their value ranges depend on the size of space they occupy and their representation method.
    • Sign-magnitude, 1's complement, and 2's complement are three methods for encoding numbers in computers, and they can be converted into each other. The most significant bit of sign-magnitude is the sign bit, and the remaining bits represent the value of the number.
    • Integers are stored in computers in 2's complement form. Under 2's complement representation, computers can treat the addition of positive and negative numbers uniformly, without needing to design special hardware circuits for subtraction, and there is no ambiguity of positive and negative zero.
    • The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bits, the range of floating-point numbers is much larger than that of integers, at the cost of sacrificing precision.
    • ASCII is the earliest English character set, with a length of 1 byte, containing a total of 128 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods.
    • UTF-8 is the most popular Unicode encoding method and has excellent compatibility. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are common Unicode encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default.
    ","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Why do hash tables contain both linear and non-linear data structures?

    The underlying structure of a hash table is an array. To resolve hash collisions, we may use \"chaining\" (discussed in the subsequent \"Hash Collision\" section): each bucket in the array points to a linked list, which may be converted to a tree (usually a red-black tree) when the list length exceeds a certain threshold.

    From a storage perspective, the underlying structure of a hash table is an array, where each bucket slot may contain a value, a linked list, or a tree. Therefore, hash tables may contain both linear data structures (arrays, linked lists) and non-linear data structures (trees).

    Q: Is the length of the char type 1 byte?

    The length of the char type is determined by the encoding method used by the programming language. For example, Java, JavaScript, TypeScript, and C# all use UTF-16 encoding (to store Unicode code points), so the char type has a length of 2 bytes.

    Q: Is there ambiguity in referring to array-based data structures as \"static data structures\"? Stacks can also perform \"dynamic\" operations such as push and pop.

    Stacks can indeed implement dynamic data operations, but the data structure is still \"static\" (fixed length). Although array-based data structures can dynamically add or remove elements, their capacity is fixed. If the data volume exceeds the pre-allocated size, a new larger array needs to be created, and the contents of the old array must be copied to the new array.

    Q: When constructing a stack (queue), its size is not specified. Why are they \"static data structures\"?

    In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); the class handles this automatically. For example, the initial capacity of Java's ArrayList is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent \"List\" section for details.

    Q: The method of converting sign-magnitude to 2's complement is \"first negate then add 1\". So converting 2's complement to sign-magnitude should be the inverse operation \"first subtract 1 then negate\". However, 2's complement can also be converted to sign-magnitude through \"first negate then add 1\". Why is this?

    This is because the mutual conversion between sign-magnitude and 2's complement is actually the process of computing the \"complement\". Let us first define the complement: assuming \\(a + b = c\\), then we say that \\(a\\) is the complement of \\(b\\) to \\(c\\), and conversely, \\(b\\) is the complement of \\(a\\) to \\(c\\).

    Given an \\(n = 4\\) bit binary number \\(0010\\), if we treat this number as sign-magnitude (ignoring the sign bit), then its 2's complement can be obtained through \"first negate then add 1\":

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

    We find that the sum of sign-magnitude and 2's complement is \\(0010 + 1110 = 10000\\), which means the 2's complement \\(1110\\) is the \"complement\" of sign-magnitude \\(0010\\) to \\(10000\\). This means the above \"first negate then add 1\" is actually the process of computing the complement to \\(10000\\).

    So, what is the \"complement\" of 2's complement \\(1110\\) to \\(10000\\)? We can still use \"first negate then add 1\" to obtain it:

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

    In other words, sign-magnitude and 2's complement are each other's \"complement\" to \\(10000\\), so \"sign-magnitude to 2's complement\" and \"2's complement to sign-magnitude\" can be implemented using the same operation (first negate then add 1).

    Of course, we can also use the inverse operation to find the sign-magnitude of 2's complement \\(1110\\), that is, \"first subtract 1 then negate\":

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

    In summary, both \"first negate then add 1\" and \"first subtract 1 then negate\" are computing the complement to \\(10000\\), and they are equivalent.

    Essentially, the \"negate\" operation is actually finding the complement to \\(1111\\) (because \"sign-magnitude + 1's complement = 1111\" always holds); and adding 1 to the 1's complement yields the 2's complement, which is the complement to \\(10000\\).

    The above uses \\(n = 4\\) as an example, and it can be generalized to binary numbers of any number of bits.

    ","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"Chapter 12.   Divide and Conquer","text":"

    Abstract

    Difficult problems are decomposed layer by layer, with each decomposition making them simpler.

    Divide and conquer reveals an important truth: start with what is simple, and nothing remains complex.

    ","path":["Chapter 12. Divide and Conquer","Chapter 12.   Divide and Conquer"],"tags":[]},{"location":"chapter_divide_and_conquer/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 12.1   Divide and Conquer Algorithms
    • 12.2   Divide and Conquer Search Strategy
    • 12.3   Building a Binary Tree Problem
    • 12.4   Hanota Problem
    • 12.5   Summary
    ","path":["Chapter 12. Divide and Conquer","Chapter 12.   Divide and Conquer"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   Divide and Conquer Search Strategy","text":"

    We have already learned that search algorithms are divided into two major categories.

    • Brute-force search: Implemented by traversing the data structure, with a time complexity of \\(O(n)\\).
    • Adaptive search: Leverages specific data organization or prior information, with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\).

    In fact, search algorithms with time complexity of \\(O(\\log n)\\) are typically implemented based on the divide and conquer strategy, such as binary search and trees.

    • Each step of binary search divides the problem (searching for a target element in an array) into a smaller problem (searching for the target element in half of the array), continuing until the array is empty or the target element is found.
    • Trees are representative of the divide and conquer idea. In data structures such as binary search trees, AVL trees, and heaps, the time complexity of various operations is \\(O(\\log n)\\).

    The divide and conquer strategy of binary search is as follows.

    • The problem can be decomposed: Binary search recursively decomposes the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element.
    • Subproblems are independent: In binary search, each round only processes one subproblem, which is not affected by other subproblems.
    • Solutions of subproblems do not need to be merged: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved.

    Divide and conquer can improve search efficiency because brute-force search can only eliminate one option per round, while divide and conquer search can eliminate half of the options per round.

    ","path":["Chapter 12. Divide and Conquer","12.2   Divide and Conquer Search Strategy"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1-implementing-binary-search-based-on-divide-and-conquer","level":3,"title":"1.   Implementing Binary Search Based on Divide and Conquer","text":"

    In previous sections, binary search was implemented based on iteration. Now we implement it based on divide and conquer (recursion).

    Question

    Given a sorted array nums of length \\(n\\), where all elements are unique, find target.

    From a divide and conquer perspective, we denote the subproblem corresponding to the search interval \\([i, j]\\) as \\(f(i, j)\\).

    Starting from the original problem \\(f(0, n-1)\\), perform binary search through the following steps.

    1. Calculate the midpoint \\(m\\) of the search interval \\([i, j]\\), and use it to eliminate half of the search interval.
    2. Recursively solve the subproblem reduced by half in size, which could be \\(f(i, m-1)\\) or \\(f(m+1, j)\\).
    3. Repeat steps 1. and 2. until target is found, or return when the interval is empty.

    Figure 12-4 shows the divide and conquer process of binary search for element \\(6\\) in an array.

    Figure 12-4   Divide and conquer process of binary search

    In the implementation code, we declare a recursive function dfs() to solve the problem \\(f(i, j)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py
    def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n    \"\"\"Binary search: problem f(i, j)\"\"\"\n    # If the interval is empty, it means there is no target element, return -1\n    if i > j:\n        return -1\n    # Calculate the midpoint index m\n    m = (i + j) // 2\n    if nums[m] < target:\n        # Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j)\n    elif nums[m] > target:\n        # Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1)\n    else:\n        # Found the target element, return its index\n        return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Binary search\"\"\"\n    n = len(nums)\n    # Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n
    binary_search_recur.cpp
    /* Binary search: problem f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(vector<int> &nums, int target) {\n    int n = nums.size();\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.java
    /* Binary search: problem f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(int[] nums, int target) {\n    int n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.cs
    /* Binary search: problem f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return DFS(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return DFS(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint BinarySearch(int[] nums, int target) {\n    int n = nums.Length;\n    // Solve the problem f(0, n-1)\n    return DFS(nums, target, 0, n - 1);\n}\n
    binary_search_recur.go
    /* Binary search: problem f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n    // If interval is empty, indicating no target element, return -1\n    if i > j {\n        return -1\n    }\n    // Calculate midpoint index\n    m := i + ((j - i) >> 1)\n    // Compare midpoint with target element\n    if nums[m] < target {\n        // If smaller, recurse on right half of array\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m+1, j)\n    } else if nums[m] > target {\n        // If larger, recurse on left half of array\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m-1)\n    } else {\n        // Found the target element, return its index\n        return m\n    }\n}\n\n/* Binary search */\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
    /* Binary search: problem f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {\n    // If the interval is empty, it means there is no target element, return -1\n    if i > j {\n        return -1\n    }\n    // Calculate the midpoint index m\n    let m = (i + j) / 2\n    if nums[m] < target {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums: nums, target: target, i: m + 1, j: j)\n    } else if nums[m] > target {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums: nums, target: target, i: i, j: m - 1)\n    } else {\n        // Found the target element, return its index\n        return m\n    }\n}\n\n/* Binary search */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Solve the problem f(0, n-1)\n    dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n
    binary_search_recur.js
    /* Binary search: problem f(i, j) */\nfunction dfs(nums, target, i, j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfunction binarySearch(nums, target) {\n    const n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.ts
    /* Binary search: problem f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfunction binarySearch(nums: number[], target: number): number {\n    const n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.dart
    /* Binary search: problem f(i, j) */\nint dfs(List<int> nums, int target, int i, int j) {\n  // If the interval is empty, it means there is no target element, return -1\n  if (i > j) {\n    return -1;\n  }\n  // Calculate the midpoint index m\n  int m = (i + j) ~/ 2;\n  if (nums[m] < target) {\n    // Recursion subproblem f(m+1, j)\n    return dfs(nums, target, m + 1, j);\n  } else if (nums[m] > target) {\n    // Recursion subproblem f(i, m-1)\n    return dfs(nums, target, i, m - 1);\n  } else {\n    // Found the target element, return its index\n    return m;\n  }\n}\n\n/* Binary search */\nint binarySearch(List<int> nums, int target) {\n  int n = nums.length;\n  // Solve the problem f(0, n-1)\n  return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.rs
    /* Binary search: problem f(i, j) */\nfn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {\n    // If the interval is empty, it means there is no target element, return -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        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if nums[m as usize] > target {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    let n = nums.len() as i32;\n    // Solve the problem f(0, n-1)\n    dfs(nums, target, 0, n - 1)\n}\n
    binary_search_recur.c
    /* Binary search: problem f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(int nums[], int target, int numsSize) {\n    int n = numsSize;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.kt
    /* Binary search: problem f(i, j) */\nfun dfs(\n    nums: IntArray,\n    target: Int,\n    i: Int,\n    j: Int\n): Int {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1\n    }\n    // Calculate the midpoint index m\n    val m = (i + j) / 2\n    return if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        dfs(nums, target, m + 1, j)\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        dfs(nums, target, i, m - 1)\n    } else {\n        // Found the target element, return its index\n        m\n    }\n}\n\n/* Binary search */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    val n = nums.size\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n}\n
    binary_search_recur.rb
    ### Binary search: problem f(i, j) ###\ndef dfs(nums, target, i, j)\n  # If the interval is empty, it means there is no target element, return -1\n  return -1 if i > j\n\n  # Calculate the midpoint index m\n  m = (i + j) / 2\n\n  if nums[m] < target\n    # Recursion subproblem f(m+1, j)\n    return dfs(nums, target, m + 1, j)\n  elsif nums[m] > target\n    # Recursion subproblem f(i, m-1)\n    return dfs(nums, target, i, m - 1)\n  else\n    # Found the target element, return its index\n    return m\n  end\nend\n\n### Binary search ###\ndef binary_search(nums, target)\n  n = nums.length\n  # Solve the problem f(0, n-1)\n  dfs(nums, target, 0, n - 1)\nend\n
    ","path":["Chapter 12. Divide and Conquer","12.2   Divide and Conquer Search Strategy"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   Building a Binary Tree Problem","text":"

    Question

    Given the preorder traversal preorder and inorder traversal inorder of a binary tree, construct the binary tree and return the root node of the binary tree. Assume there are no duplicate node values in the binary tree (as shown in Figure 12-5).

    Figure 12-5   Example data for building a binary tree

    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1-determining-if-it-is-a-divide-and-conquer-problem","level":3,"title":"1.   Determining If It Is a Divide and Conquer Problem","text":"

    The original problem is defined as constructing a binary tree from preorder and inorder, which is a typical divide and conquer problem.

    • The problem can be decomposed: From a divide and conquer perspective, we can divide the original problem into two subproblems: constructing the left subtree and constructing the right subtree, plus one operation: initializing the root node. For each subtree (subproblem), we can still reuse the above division method, dividing it into smaller subtrees (subproblems) until the smallest subproblem (empty subtree) is reached.
    • Subproblems are independent: The left and right subtrees are independent of each other; there is no overlap between them. When constructing the left subtree, we only need to focus on the parts of the inorder and preorder traversals corresponding to the left subtree. The same applies to the right subtree.
    • Solutions of subproblems can be merged: Once we have the left and right subtrees (solutions of subproblems), we can link them to the root node to obtain the solution to the original problem.
    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2-how-to-divide-subtrees","level":3,"title":"2.   How to Divide Subtrees","text":"

    Based on the above analysis, this problem can be solved using divide and conquer, but how do we divide the left and right subtrees through the preorder traversal preorder and inorder traversal inorder?

    According to the definition, both preorder and inorder can be divided into three parts.

    • Preorder traversal: [ Root Node | Left Subtree | Right Subtree ], for example, the tree in Figure 12-5 corresponds to [ 3 | 9 | 2 1 7 ].
    • Inorder traversal: [ Left Subtree | Root Node | Right Subtree ], for example, the tree in Figure 12-5 corresponds to [ 9 | 3 | 1 2 7 ].

    Using the data from the figure above as an example, we can obtain the division results through the steps shown in Figure 12-6.

    1. The first element 3 in the preorder traversal is the value of the root node.
    2. Find the index of root node 3 in inorder, and use this index to divide inorder into [ 9 | 3 | 1 2 7 ].
    3. Based on the division result of inorder, it is easy to determine that the left and right subtrees have 1 and 3 nodes respectively, allowing us to divide preorder into [ 3 | 9 | 2 1 7 ].

    Figure 12-6   Dividing subtrees in preorder and inorder traversals

    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3-describing-subtree-intervals-based-on-variables","level":3,"title":"3.   Describing Subtree Intervals Based on Variables","text":"

    Based on the above division method, we have obtained the index intervals of the root node, left subtree, and right subtree in preorder and inorder. To describe these index intervals, we need to use several index variables.

    • Denote the index of the current tree's root node in preorder as \\(i\\).
    • Denote the index of the current tree's root node in inorder as \\(m\\).
    • Denote the index interval of the current tree in inorder as \\([l, r]\\).

    As shown in Table 12-1, through these variables we can represent the index of the root node in preorder and the index intervals of the subtrees in inorder.

    Table 12-1   Indices of root node and subtrees in preorder and inorder traversals

    Root node index in preorder Subtree index interval in inorder Current tree \\(i\\) \\([l, r]\\) Left subtree \\(i + 1\\) \\([l, m-1]\\) Right subtree \\(i + 1 + (m - l)\\) \\([m+1, r]\\)

    Please note that \\((m-l)\\) in the right subtree root node index means \"the number of nodes in the left subtree\". It is recommended to understand this in conjunction with Figure 12-7.

    Figure 12-7   Index interval representation of root node and left and right subtrees

    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4-code-implementation","level":3,"title":"4.   Code Implementation","text":"

    To improve the efficiency of querying \\(m\\), we use a hash table hmap to store the mapping from elements in the inorder array to their indices:

    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    \"\"\"Build binary tree: divide and conquer\"\"\"\n    # Terminate when the subtree interval is empty\n    if r - l < 0:\n        return None\n    # Initialize the root node\n    root = TreeNode(preorder[i])\n    # Query m to divide the left and right subtrees\n    m = inorder_map[preorder[i]]\n    # Subproblem: build the left subtree\n    root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n    # Subproblem: build the right subtree\n    root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n    # Return the root node\n    return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n    \"\"\"Build binary tree\"\"\"\n    # Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return NULL;\n    // Initialize the root node\n    TreeNode *root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return null;\n    // Initialize the root node\n    TreeNode root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return null;\n    // Initialize the root node\n    TreeNode root = new(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n    // Terminate when the subtree interval is empty\n    if r-l < 0 {\n        return nil\n    }\n    // Initialize the root node\n    root := NewTreeNode(preorder[i])\n    // Query m to divide the left and right subtrees\n    m := inorderMap[preorder[i]]\n    // Subproblem: build the left subtree\n    root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n    // Subproblem: build the right subtree\n    root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {\n    // Terminate when the subtree interval is empty\n    if r - l < 0 {\n        return nil\n    }\n    // Initialize the root node\n    let root = TreeNode(x: preorder[i])\n    // Query m to divide the left and right subtrees\n    let m = inorderMap[preorder[i]]!\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfunc buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfunction dfs(preorder, inorderMap, i, l, r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null;\n    // Initialize the root node\n    const root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    const m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nfunction buildTree(preorder, inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfunction dfs(\n    preorder: number[],\n    inorderMap: Map<number, number>,\n    i: number,\n    l: number,\n    r: number\n): TreeNode | null {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null;\n    // Initialize the root node\n    const root: TreeNode = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    const m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode? dfs(\n  List<int> preorder,\n  Map<int, int> inorderMap,\n  int i,\n  int l,\n  int r,\n) {\n  // Terminate when the subtree interval is empty\n  if (r - l < 0) {\n    return null;\n  }\n  // Initialize the root node\n  TreeNode? root = TreeNode(preorder[i]);\n  // Query m to divide the left and right subtrees\n  int m = inorderMap[preorder[i]]!;\n  // Subproblem: build the left subtree\n  root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n  // Subproblem: build the right subtree\n  root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n  // Return the root node\n  return root;\n}\n\n/* Build binary tree */\nTreeNode? buildTree(List<int> preorder, List<int> inorder) {\n  // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\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    // Terminate when the subtree interval is empty\n    if r - l < 0 {\n        return None;\n    }\n    // Initialize the root node\n    let root = TreeNode::new(preorder[i as usize]);\n    // Query m to divide the left and right subtrees\n    let m = inorder_map.get(&preorder[i as usize]).unwrap();\n    // Subproblem: build the left subtree\n    root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    Some(root)\n}\n\n/* Build binary tree */\nfn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return NULL;\n    // Initialize the root node\n    TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n    root->val = preorder[i];\n    root->left = NULL;\n    root->right = NULL;\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n    // Subproblem: build the right subtree\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfun dfs(\n    preorder: IntArray,\n    inorderMap: Map<Int?, Int?>,\n    i: Int,\n    l: Int,\n    r: Int\n): TreeNode? {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null\n    // Initialize the root node\n    val root = TreeNode(preorder[i])\n    // Query m to divide the left and right subtrees\n    val m = inorderMap[preorder[i]]!!\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    ### Build binary tree: divide and conquer ###\ndef dfs(preorder, inorder_map, i, l, r)\n  # Terminate when the subtree interval is empty\n  return if r - l < 0\n\n  # Initialize the root node\n  root = TreeNode.new(preorder[i])\n  # Query m to divide the left and right subtrees\n  m = inorder_map[preorder[i]]\n  # Subproblem: build the left subtree\n  root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n  # Subproblem: build the right subtree\n  root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n  # Return the root node\n  root\nend\n\n### Build binary tree ###\ndef build_tree(preorder, inorder)\n  # Initialize hash map, storing the mapping from inorder elements to indices\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

    Figure 12-8 shows the recursive process of building the binary tree. Each node is established during the downward \"recursion\" process, while each edge (reference) is established during the upward \"return\" process.

    <1><2><3><4><5><6><7><8><9>

    Figure 12-8   Recursive process of building a binary tree

    The division results of the preorder traversal preorder and inorder traversal inorder within each recursive function are shown in Figure 12-9.

    Figure 12-9   Division results in each recursive function

    Let the number of nodes in the tree be \\(n\\). Initializing each node (executing one recursive function dfs()) takes \\(O(1)\\) time. Therefore, the overall time complexity is \\(O(n)\\).

    The hash table stores the mapping from inorder elements to their indices, with a space complexity of \\(O(n)\\). In the worst case, when the binary tree degenerates into a linked list, the recursion depth reaches \\(n\\), using \\(O(n)\\) stack frame space. Therefore, the overall space complexity is \\(O(n)\\).

    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   Divide and Conquer Algorithms","text":"

    Divide and conquer is a very important and common algorithmic strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: \"divide\" and \"conquer\".

    1. Divide (partition phase): Recursively divide the original problem into two or more subproblems until the smallest subproblem is reached.
    2. Conquer (merge phase): Starting from the smallest subproblems with known solutions, merge the solutions of subproblems from bottom to top to construct the solution to the original problem.

    As shown in Figure 12-1, \"merge sort\" is one of the typical applications of the divide and conquer strategy.

    1. Divide: Recursively divide the original array (original problem) into two subarrays (subproblems) until the subarray has only one element (smallest subproblem).
    2. Conquer: Merge the sorted subarrays (solutions to subproblems) from bottom to top to obtain a sorted original array (solution to the original problem).

    Figure 12-1   Divide and conquer strategy of merge sort

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211-how-to-determine-divide-and-conquer-problems","level":2,"title":"12.1.1   How to Determine Divide and Conquer Problems","text":"

    Whether a problem is suitable for solving with divide and conquer can usually be determined based on the following criteria.

    1. The problem can be decomposed: The original problem can be divided into smaller, similar subproblems, and can be recursively divided in the same way.
    2. Subproblems are independent: There is no overlap between subproblems, they are independent of each other and can be solved independently.
    3. Solutions of subproblems can be merged: The solution to the original problem is obtained by merging the solutions of subproblems.

    Clearly, merge sort satisfies these three criteria.

    1. The problem can be decomposed: Recursively divide the array (original problem) into two subarrays (subproblems).
    2. Subproblems are independent: Each subarray can be sorted independently (subproblems can be solved independently).
    3. Solutions of subproblems can be merged: Two sorted subarrays (solutions of subproblems) can be merged into one sorted array (solution of the original problem).
    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212-improving-efficiency-through-divide-and-conquer","level":2,"title":"12.1.2   Improving Efficiency Through Divide and Conquer","text":"

    Divide and conquer can not only effectively solve algorithmic problems, but can often also improve algorithmic efficiency. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy.

    This raises the question: Why can divide and conquer improve algorithm efficiency, and what is the underlying logic? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation.

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1-operation-count-optimization","level":3,"title":"1.   Operation Count Optimization","text":"

    Taking \"bubble sort\" as an example, processing an array of length \\(n\\) requires \\(O(n^2)\\) time. Suppose we divide the array at the midpoint into two subarrays, as shown in Figure 12-2. The division requires \\(O(n)\\) time, sorting each subarray requires \\(O((n / 2)^2)\\) time, and merging the two subarrays requires \\(O(n)\\) time, resulting in an overall time complexity of:

    \\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\]

    Figure 12-2   Bubble sort before and after array division

    Next, we compute the following inequality, where the left and right sides represent the total number of operations before and after division, respectively:

    \\[ \\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} \\]

    This means that when \\(n > 4\\), the number of operations after division is smaller, and sorting efficiency should be higher. Note that the time complexity after division is still quadratic \\(O(n^2)\\), but the constant term in the complexity has become smaller.

    Going further, what if we continuously divide the subarrays from their midpoints into two subarrays until the subarrays have only one element? This approach is actually \"merge sort\", with a time complexity of \\(O(n \\log n)\\).

    Thinking further, what if we set multiple division points and evenly divide the original array into \\(k\\) subarrays? This situation is very similar to \"bucket sort\", which is well-suited for sorting massive amounts of data, with a theoretical time complexity of \\(O(n + k)\\).

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2-parallel-computation-optimization","level":3,"title":"2.   Parallel Computation Optimization","text":"

    We know that the subproblems generated by divide and conquer are independent of each other, so they can typically be solved in parallel. This means divide and conquer can not only reduce the time complexity of algorithms, but is also amenable to parallel optimization by the operating system.

    Parallel optimization is particularly effective in multi-core or multi-processor environments, as the system can simultaneously handle multiple subproblems, making fuller use of computing resources and significantly reducing overall runtime.

    For example, in the \"bucket sort\" shown in Figure 12-3, we evenly distribute massive data into various buckets, and the sorting tasks for all buckets can be distributed to various computing units. After completion, the results are merged.

    Figure 12-3   Parallel computation in bucket sort

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213-common-applications-of-divide-and-conquer","level":2,"title":"12.1.3   Common Applications of Divide and Conquer","text":"

    On the one hand, divide and conquer can be used to solve many classic algorithmic problems.

    • Finding the closest pair of points: This algorithm first divides the point set into two parts, then finds the closest pair of points in each part separately, and finally finds the closest pair of points that spans both parts.
    • Large integer multiplication: For example, the Karatsuba algorithm, which decomposes large integer multiplication into several smaller integer multiplications and additions.
    • Matrix multiplication: For example, the Strassen algorithm, which decomposes large matrix multiplication into multiple small matrix multiplications and additions.
    • Hanota problem: The hanota problem can be solved through recursion, which is a typical application of the divide and conquer strategy.
    • Solving inversion pairs: In a sequence, if a preceding number is greater than a following number, these two numbers form an inversion pair. Solving the inversion pair problem can utilize the divide and conquer approach with the help of merge sort.

    On the other hand, divide and conquer is widely applied in the design of algorithms and data structures.

    • Binary search: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary-search step on the remaining interval.
    • Merge sort: Already introduced at the beginning of this section, no further elaboration needed.
    • Quick sort: Quick sort selects a pivot value, then divides the array into two subarrays, one with elements smaller than the pivot and the other with elements larger than the pivot, then performs the same division operation on these two parts until the subarrays have only one element.
    • Bucket sort: The basic idea of bucket sort is to scatter data into multiple buckets, then sort the elements within each bucket, and finally extract the elements from each bucket in sequence to obtain a sorted array.
    • Trees: For example, binary search trees, AVL trees, red-black trees, B-trees, B+ trees, etc. Their search, insertion, and deletion operations can all be viewed as applications of the divide and conquer strategy.
    • Heaps: A heap is a special complete binary tree, and its various operations, such as insertion, deletion, and heapify, actually imply the divide and conquer idea.
    • Hash tables: Although hash tables do not directly apply divide and conquer, some methods for resolving hash collisions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve lookup efficiency.

    It can be seen that divide and conquer is a \"quietly pervasive\" algorithmic idea, embedded in various algorithms and data structures.

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   Hanota Problem","text":"

    In merge sort and building binary trees, we decompose the original problem into two subproblems, each half the size of the original problem. However, for the hanota problem, we adopt a different decomposition strategy.

    Question

    Given three pillars, denoted as A, B, and C. Initially, pillar A has \\(n\\) discs stacked on it, arranged from top to bottom in ascending order of size. Our task is to move these \\(n\\) discs to pillar C while maintaining their original order (as shown in Figure 12-10). The following rules must be followed when moving the discs.

    1. A disc can only be taken from the top of one pillar and placed on top of another pillar.
    2. Only one disc can be moved at a time.
    3. A smaller disc must always be on top of a larger disc.

    Figure 12-10   Example of the hanota problem

    We denote the hanota problem of size \\(i\\) as \\(f(i)\\). For example, \\(f(3)\\) represents moving \\(3\\) discs from A to C.

    ","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1-considering-the-base-cases","level":3,"title":"1.   Considering the Base Cases","text":"

    As shown in Figure 12-11, for problem \\(f(1)\\), when there is only one disc, we can move it directly from A to C.

    <1><2>

    Figure 12-11   Solution for a problem of size 1

    As shown in Figure 12-12, for problem \\(f(2)\\), when there are two discs, since we must always keep the smaller disc on top of the larger disc, we need to use B to assist in the move.

    1. First, move the smaller disc from A to B.
    2. Then move the larger disc from A to C.
    3. Finally, move the smaller disc from B to C.
    <1><2><3><4>

    Figure 12-12   Solution for a problem of size 2

    The process of solving problem \\(f(2)\\) can be summarized as: moving two discs from A to C with the help of B. Here, C is called the target pillar, and B is called the buffer pillar.

    ","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2-subproblem-decomposition","level":3,"title":"2.   Subproblem Decomposition","text":"

    For problem \\(f(3)\\), when there are three discs, the situation becomes slightly more complex.

    Since we already know the solutions to \\(f(1)\\) and \\(f(2)\\), we can think from a divide and conquer perspective, treating the top two discs on A as a whole, and execute the steps shown in Figure 12-13. This successfully moves the three discs from A to C.

    1. Let B be the target pillar and C be the buffer pillar, and move two discs from A to B.
    2. Move the remaining disc from A directly to C.
    3. Let C be the target pillar and A be the buffer pillar, and move two discs from B to C.
    <1><2><3><4>

    Figure 12-13   Solution for a problem of size 3

    Essentially, we divide problem \\(f(3)\\) into two subproblems \\(f(2)\\) and one subproblem \\(f(1)\\). By solving these three subproblems in order, the original problem is solved. This shows that the subproblems are independent and their solutions can be merged.

    From this, we can summarize the divide and conquer strategy for solving the hanota problem shown in Figure 12-14: divide the original problem \\(f(n)\\) into two subproblems \\(f(n-1)\\) and one subproblem \\(f(1)\\), and solve these three subproblems in the following order.

    1. Move \\(n-1\\) discs from A to B with the help of C.
    2. Move the remaining \\(1\\) disc directly from A to C.
    3. Move \\(n-1\\) discs from B to C with the help of A.

    For these two subproblems \\(f(n-1)\\), we can recursively divide them in the same way until reaching the smallest subproblem \\(f(1)\\). The solution to \\(f(1)\\) is known and requires only one move operation.

    Figure 12-14   Divide and conquer strategy for solving the hanota problem

    ","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

    In the code, we declare a recursive function dfs(i, src, buf, tar), whose purpose is to move the top \\(i\\) discs from pillar src to target pillar tar with the help of buffer pillar buf:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
    def move(src: list[int], tar: list[int]):\n    \"\"\"Move a disk\"\"\"\n    # Take out a disk from the top of src\n    pan = src.pop()\n    # Place the disk on top of tar\n    tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n    \"\"\"Solve the Tower of Hanoi problem f(i)\"\"\"\n    # If there is only one disk left in src, move it directly to tar\n    if i == 1:\n        move(src, tar)\n        return\n    # Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf)\n    # Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    # Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n    \"\"\"Solve the Tower of Hanoi problem\"\"\"\n    n = len(A)\n    # Move the top n disks from A to C using B\n    dfs(n, A, B, C)\n
    hanota.cpp
    /* Move a disk */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // Take out a disk from the top of src\n    int pan = src.back();\n    src.pop_back();\n    // Place the disk on top of tar\n    tar.push_back(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n    int n = A.size();\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.java
    /* Move a disk */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // Take out a disk from the top of src\n    Integer pan = src.remove(src.size() - 1);\n    // Place the disk on top of tar\n    tar.add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n    int n = A.size();\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.cs
    /* Move a disk */\nvoid Move(List<int> src, List<int> tar) {\n    // Take out a disk from the top of src\n    int pan = src[^1];\n    src.RemoveAt(src.Count - 1);\n    // Place the disk on top of tar\n    tar.Add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid DFS(int i, List<int> src, List<int> buf, List<int> tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    DFS(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    Move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    DFS(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid SolveHanota(List<int> A, List<int> B, List<int> C) {\n    int n = A.Count;\n    // Move the top n disks from A to C using B\n    DFS(n, A, B, C);\n}\n
    hanota.go
    /* Move a disk */\nfunc move(src, tar *list.List) {\n    // Take out a disk from the top of src\n    pan := src.Back()\n    // Place the disk on top of tar\n    tar.PushBack(pan.Value)\n    // Remove top disk from src\n    src.Remove(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfsHanota(i-1, src, tar, buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // Move the top n disks from A to C using B\n    dfsHanota(n, A, B, C)\n}\n
    hanota.swift
    /* Move a disk */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // Take out a disk from the top of src\n    let pan = src.popLast()!\n    // Place the disk on top of tar\n    tar.append(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src: &src, tar: &tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n    let n = A.count\n    // The tail of the list is the top of the rod\n    // Move top n disks from src to C using B\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
    hanota.js
    /* Move a disk */\nfunction move(src, tar) {\n    // Take out a disk from the top of src\n    const pan = src.pop();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunction dfs(i, src, buf, tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.ts
    /* Move a disk */\nfunction move(src: number[], tar: number[]): void {\n    // Take out a disk from the top of src\n    const pan = src.pop();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n    // If there is only one disk left in src, move it directly to tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n    const n = A.length;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.dart
    /* Move a disk */\nvoid move(List<int> src, List<int> tar) {\n  // Take out a disk from the top of src\n  int pan = src.removeLast();\n  // Place the disk on top of tar\n  tar.add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, List<int> src, List<int> buf, List<int> tar) {\n  // If there is only one disk left in src, move it directly to tar\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n  dfs(i - 1, src, tar, buf);\n  // Subproblem f(1): move the remaining disk from src to tar\n  move(src, tar);\n  // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n  dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(List<int> A, List<int> B, List<int> C) {\n  int n = A.length;\n  // Move the top n disks from A to C using B\n  dfs(n, A, B, C);\n}\n
    hanota.rs
    /* Move a disk */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // Take out a disk from the top of src\n    let pan = src.pop().unwrap();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move_pan(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {\n    let n = A.len() as i32;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.c
    /* Move a disk */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // Take out a disk from the top of src\n    int pan = src[*srcSize - 1];\n    src[*srcSize - 1] = 0;\n    (*srcSize)--;\n    // Place the disk on top of tar\n    tar[*tarSize] = pan;\n    (*tarSize)++;\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, srcSize, tar, tarSize);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n    // Move the top n disks from A to C using B\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
    hanota.kt
    /* Move a disk */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // Take out a disk from the top of src\n    val pan = src.removeAt(src.size - 1)\n    // Place the disk on top of tar\n    tar.add(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {\n    val n = A.size\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C)\n}\n
    hanota.rb
    ### Move one disk ###\ndef move(src, tar)\n  # Take out a disk from the top of src\n  pan = src.pop\n  # Place the disk on top of tar\n  tar << pan\nend\n\n### Solve Tower of Hanoi f(i) ###\ndef dfs(i, src, buf, tar)\n  # If there is only one disk left in src, move it directly to tar\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n  dfs(i - 1, src, tar, buf)\n  # Subproblem f(1): move the remaining disk from src to tar\n  move(src, tar)\n  # Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n  dfs(i - 1, buf, src, tar)\nend\n\n### Solve Tower of Hanoi ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # Move the top n disks from A to C using B\n  dfs(n, _A, _B, _C)\nend\n

    As shown in Figure 12-15, the hanota problem forms a recursion tree of height \\(n\\), where each node represents a subproblem corresponding to an invocation of the dfs() function, therefore the time complexity is \\(O(2^n)\\) and the space complexity is \\(O(n)\\).

    Figure 12-15   Recursion tree of the hanota problem

    Quote

    The hanota problem originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and \\(64\\) golden discs of different sizes. The monks continuously moved the discs, believing that when the last disc was correctly placed, the world would come to an end.

    However, even if the monks moved one disc per second, it would take approximately \\(2^{64} \\approx 1.84×10^{19}\\) seconds, which is about \\(585\\) billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world.

    ","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   Summary","text":"","path":["Chapter 12. Divide and Conquer","12.5   Summary"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Divide and conquer is a common algorithm design strategy consisting of two phases, divide (partition) and conquer (merge), and is typically implemented recursively.
    • The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged.
    • Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting.
    • Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, it reduces the number of operations; on the other hand, it makes parallel optimization by the system easier.
    • Divide and conquer can solve many algorithmic problems and is also widely used in data structures and algorithm design, making it ubiquitous.
    • Compared to brute-force search, adaptive search is more efficient. Search algorithms with time complexity of \\(O(\\log n)\\) are typically implemented based on the divide and conquer strategy.
    • Binary search is another typical application of divide and conquer. It does not include the step of merging solutions of subproblems. We can implement binary search through recursive divide and conquer.
    • In the problem of building a binary tree, building the tree (original problem) can be divided into building the left subtree and right subtree (subproblems), which can be achieved by dividing the index intervals of the preorder and inorder traversals.
    • In the hanota problem, a problem of size \\(n\\) can be divided into two subproblems of size \\(n-1\\) and one subproblem of size \\(1\\). After solving these three subproblems in order, the original problem is solved.
    ","path":["Chapter 12. Divide and Conquer","12.5   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"Chapter 14.   Dynamic Programming","text":"

    Abstract

    Streams flow into rivers, rivers flow into the sea.

    Dynamic programming combines solutions to small problems into the answer to a large problem, leading us step by step to the other shore of problem-solving.

    ","path":["Chapter 14. Dynamic Programming","Chapter 14.   Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 14.1   Introduction to Dynamic Programming
    • 14.2   Characteristics of Dynamic Programming Problems
    • 14.3   Dynamic Programming Problem-Solving Approach
    • 14.4   0-1 Knapsack Problem
    • 14.5   Unbounded Knapsack Problem
    • 14.6   Edit Distance Problem
    • 14.7   Summary
    ","path":["Chapter 14. Dynamic Programming","Chapter 14.   Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   Characteristics of Dynamic Programming Problems","text":"

    In the previous section, we learned how dynamic programming solves the original problem by decomposing it into subproblems. In fact, subproblem decomposition is a general algorithmic approach, with different emphases in divide and conquer, dynamic programming, and backtracking.

    • Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and merge the solutions to the subproblems during backtracking to ultimately obtain the solution to the original problem.
    • Dynamic programming also recursively decomposes problems, but the main difference from divide and conquer algorithms is that subproblems in dynamic programming are interdependent, and many overlapping subproblems appear during the decomposition process.
    • Backtracking algorithms enumerate all possible solutions through trial and error, and avoid unnecessary search branches through pruning. The solution to the original problem consists of a series of decision steps, and we can regard the subsequence before each decision step as a subproblem.

    In fact, dynamic programming is commonly used to solve optimization problems, which not only contain overlapping subproblems but also have two other major characteristics: optimal substructure and no aftereffects.

    ","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421-optimal-substructure","level":2,"title":"14.2.1   Optimal Substructure","text":"

    We make a slight modification to the stair climbing problem to make it more suitable for demonstrating the concept of optimal substructure.

    Climbing stairs with minimum cost

    Given a staircase, you can climb \\(1\\) or \\(2\\) steps at a time, and each step is labeled with a non-negative integer representing the cost of stepping on it. Given a non-negative integer array \\(cost\\), where \\(cost[i]\\) represents the cost of the \\(i\\)-th step and \\(cost[0]\\) is the ground (starting point), what is the minimum cost required to reach the top?

    As shown in Figure 14-6, if the costs of the \\(1\\)st, \\(2\\)nd, and \\(3\\)rd steps are \\(1\\), \\(10\\), and \\(1\\) respectively, then climbing from the ground to the \\(3\\)rd step requires a minimum cost of \\(2\\).

    Figure 14-6   Minimum cost to climb to the 3rd step

    Let \\(dp[i]\\) be the accumulated cost of climbing to the \\(i\\)-th step. Since the \\(i\\)-th step can only come from the \\(i-1\\)-th or \\(i-2\\)-th step, \\(dp[i]\\) can only equal \\(dp[i-1] + cost[i]\\) or \\(dp[i-2] + cost[i]\\). To minimize the cost, we should choose the smaller of the two:

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

    This leads us to the meaning of optimal substructure: the optimal solution to the original problem is constructed from the optimal solutions to the subproblems.

    This problem clearly has optimal substructure: we select the better one from the optimal solutions to the two subproblems \\(dp[i-1]\\) and \\(dp[i-2]\\), and use it to construct the optimal solution to the original problem \\(dp[i]\\).

    So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to find the number of ways, which seems to be a counting problem, but if we change the question: \"Find the maximum number of ways\". We surprisingly discover that although the problem before and after modification are equivalent, the optimal substructure has emerged: the maximum number of ways for the \\(n\\)-th step equals the sum of the maximum number of ways for the \\(n-1\\)-th and \\(n-2\\)-th steps. Therefore, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems.

    According to the state transition equation and the initial states \\(dp[1] = cost[1]\\) and \\(dp[2] = cost[2]\\), we can obtain the dynamic programming code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
    def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n    \"\"\"Minimum cost climbing stairs: Dynamic programming\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [0] * (n + 1)\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1], dp[2] = cost[1], cost[2]\n    # State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    vector<int> dp(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint MinCostClimbingStairsDP(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\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    // Initialize dp table, used to store solutions to subproblems\n    dp := make([]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nfunc minCostClimbingStairsDP(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: 0, count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nfunction minCostClimbingStairsDP(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nfunction minCostClimbingStairsDP(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  // Initialize dp table, used to store solutions to subproblems\n  List<int> dp = List.filled(n + 1, 0);\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1] = cost[1];\n  dp[2] = cost[2];\n  // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\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    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![-1; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int *dp = calloc(n + 1, sizeof(int));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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    // Free memory\n    free(dp);\n    return res;\n}\n
    min_cost_climbing_stairs_dp.kt
    /* Minimum cost climbing stairs: Dynamic programming */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = IntArray(n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\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
    ### Minimum cost climbing stairs: DP ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = cost[1], cost[2]\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n

    Figure 14-7 shows the dynamic programming process for the above code.

    Figure 14-7   Dynamic programming process for climbing stairs with minimum cost

    This problem can also be space-optimized, compressing from one dimension to zero, reducing the space complexity from \\(O(n)\\) to \\(O(1)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
    def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n    \"\"\"Minimum cost climbing stairs: Space-optimized dynamic programming\"\"\"\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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    // Initial state: preset the solution to the smallest subproblem\n    a, b := cost[1], cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    ### Minimum cost climbing stairs: DP ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = cost[1], cost[2]\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n\n# Minimum cost climbing stairs: Space-optimized dynamic programming\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":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422-no-aftereffects","level":2,"title":"14.2.2   No Aftereffects","text":"

    No aftereffects is one of the important characteristics that enable dynamic programming to solve problems effectively. Its definition is: given a certain state, its future development is only related to the current state and has nothing to do with all past states.

    Taking the stair climbing problem as an example, given state \\(i\\), it will develop into states \\(i+1\\) and \\(i+2\\), corresponding to jumping \\(1\\) step and jumping \\(2\\) steps, respectively. When making these two choices, we do not need to consider the states before state \\(i\\), as they have no effect on the future of state \\(i\\).

    However, if we add a constraint to the stair climbing problem, the situation changes.

    Climbing stairs with constraint

    Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time, but you cannot jump \\(1\\) step in two consecutive rounds. How many ways are there to climb to the top?

    As shown in Figure 14-8, there are only \\(2\\) feasible ways to climb to the \\(3\\)rd step. The path with three consecutive \\(1\\)-step jumps does not satisfy the constraint and is therefore discarded.

    Figure 14-8   Number of ways to climb to the 3rd step with constraint

    In this problem, if the previous round was a jump of \\(1\\) step, then the next round must jump \\(2\\) steps. This means that the next choice cannot be determined solely by the current state (current stair step number), but also depends on the previous state (the stair step number from the previous round).

    It is not difficult to see that this problem no longer satisfies no aftereffects, and the state transition equation \\(dp[i] = dp[i-1] + dp[i-2]\\) also fails, because \\(dp[i-1]\\) represents jumping \\(1\\) step in this round, but it includes many solutions where \"the previous round was a jump of \\(1\\) step\", which cannot be directly counted in \\(dp[i]\\) to satisfy the constraint.

    For this reason, we need to expand the state definition: state \\([i, j]\\) represents being on the \\(i\\)-th step with the previous round having jumped \\(j\\) steps, where \\(j \\in \\{1, 2\\}\\). This state definition effectively distinguishes whether the previous round was a jump of \\(1\\) step or \\(2\\) steps, allowing us to determine where the current state came from.

    • When the previous round jumped \\(1\\) step, the round before that could only choose to jump \\(2\\) steps, i.e., \\(dp[i, 1]\\) can only transition from \\(dp[i-1, 2]\\).
    • When the previous round jumped \\(2\\) steps, the round before that could choose to jump \\(1\\) step or \\(2\\) steps, i.e., \\(dp[i, 2]\\) can transition from \\(dp[i-2, 1]\\) or \\(dp[i-2, 2]\\).

    As shown in Figure 14-9, under this definition, \\(dp[i, j]\\) represents the number of ways for state \\([i, j]\\). The state transition equation is then:

    \\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\]

    Figure 14-9   Recurrence relation considering constraints

    Finally, return \\(dp[n, 1] + dp[n, 2]\\), where the sum of the two represents the total number of ways to climb to the \\(n\\)-th step:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py
    def climbing_stairs_constraint_dp(n: int) -> int:\n    \"\"\"Climbing stairs with constraint: Dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return 1\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [[0] * 3 for _ in range(n + 1)]\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1][1], dp[1][2] = 1, 0\n    dp[2][1], dp[2][2] = 0, 1\n    # State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int[][] dp = new int[n + 1][3];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint ClimbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int[,] dp = new int[n + 1, 3];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1, 1] = 1;\n    dp[1, 2] = 0;\n    dp[2, 1] = 0;\n    dp[2, 2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfunc climbingStairsConstraintDP(n int) int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([][3]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfunc climbingStairsConstraintDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfunction climbingStairsConstraintDP(n) {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = Array.from(new Array(n + 1), () => new Array(3));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfunction climbingStairsConstraintDP(n: number): number {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = Array.from({ length: n + 1 }, () => new Array(3));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n  if (n == 1 || n == 2) {\n    return 1;\n  }\n  // Initialize dp table, used to store solutions to subproblems\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1][1] = 1;\n  dp[1][2] = 0;\n  dp[2][1] = 0;\n  dp[2][2] = 1;\n  // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfn climbing_stairs_constraint_dp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return 1;\n    };\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![vec![-1; 3]; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\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    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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    // Free memory\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
    /* Climbing stairs with constraint: Dynamic programming */\nfun climbingStairsConstraintDP(n: Int): Int {\n    if (n == 1 || n == 2) {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = Array(n + 1) { IntArray(3) }\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\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
    ### Climbing stairs with constraint: DP ###\ndef climbing_stairs_constraint_dp(n)\n  return 1 if n == 1 || n == 2\n\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1) { Array.new(3, 0) }\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1][1], dp[1][2] = 1, 0\n  dp[2][1], dp[2][2] = 0, 1\n  # State transition: gradually solve larger subproblems from smaller ones\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

    In the above case, since we only need to consider one more preceding state, we can still make the problem satisfy no aftereffects by expanding the state definition. However, some problems have very severe \"aftereffects\".

    Climbing stairs with obstacle generation

    Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time. Whenever you reach the \\(i\\)-th step, the system automatically places an obstacle on the \\(2i\\)-th step, and no subsequent round is allowed to jump to the \\(2i\\)-th step. For example, if the first two rounds jump to the \\(2\\)nd and \\(3\\)rd steps, then afterwards you cannot jump to the \\(4\\)th and \\(6\\)th steps. How many ways are there to climb to the top?

    In this problem, the next jump depends on all past states, because each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming is often difficult to solve.

    In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually use other methods, such as heuristic search, genetic algorithms, and reinforcement learning, to obtain usable locally optimal solutions within a limited time.

    ","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   Dynamic Programming Problem-Solving Approach","text":"

    The previous two sections introduced the main characteristics of dynamic programming problems. Next, let us explore two more practical issues together.

    1. How to determine whether a problem is a dynamic programming problem?
    2. What is the complete process for solving a dynamic programming problem, and where should we start?
    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431-problem-identification","level":2,"title":"14.3.1   Problem Identification","text":"

    Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and satisfies no aftereffects, then it is usually suitable for solving with dynamic programming. However, it is difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and first observe whether the problem is suitable for solving with backtracking (exhaustive search).

    Problems suitable for solving with backtracking usually satisfy the \"decision tree model\", which means the problem can be described using a tree structure, where each node represents a decision and each path represents a sequence of decisions.

    In other words, if a problem contains an explicit concept of decisions, and the solution is generated through a series of decisions, then it satisfies the decision tree model and can usually be solved using backtracking.

    On this basis, dynamic programming problems also have some positive indicators.

    • The problem contains descriptions such as maximum (minimum) or most (least), indicating optimization.
    • The problem's state can be represented using a list, multi-dimensional matrix, or tree, and a state has a recurrence relation with its surrounding states.

    Correspondingly, there are also some negative indicators.

    • The goal of the problem is to find all possible solutions, rather than finding the optimal solution.
    • The problem description has obvious permutation and combination characteristics, requiring the return of specific multiple solutions.

    If a problem satisfies the decision tree model and has relatively obvious positive indicators, we can assume it is a dynamic programming problem and verify that assumption during the solving process.

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432-problem-solving-steps","level":2,"title":"14.3.2   Problem-Solving Steps","text":"

    The problem-solving process for dynamic programming varies depending on the nature and difficulty of the problem, but generally follows these steps: describe decisions, define states, establish the \\(dp\\) table, derive state transition equations, determine boundary conditions, etc.

    To illustrate the problem-solving steps more vividly, we use a classic problem \"minimum path sum\" as an example.

    Question

    Given an \\(n \\times m\\) two-dimensional grid grid in which each cell contains a non-negative integer representing its cost, a robot starts from the top-left cell and can only move down or right at each step until reaching the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right.

    Figure 14-10 shows an example where the minimum path sum for the given grid is \\(13\\).

    Figure 14-10   Minimum path sum example data

    Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

    The decision in each round of this problem is to move one step down or right from the current cell. Let the row and column indices of the current cell be \\([i, j]\\). After moving down or right, the indices become \\([i+1, j]\\) or \\([i, j+1]\\). Therefore, the state should include two variables, the row index and column index, denoted as \\([i, j]\\).

    State \\([i, j]\\) corresponds to the subproblem: the minimum path sum from the starting point \\([0, 0]\\) to \\([i, j]\\), denoted as \\(dp[i, j]\\).

    From this, we obtain the two-dimensional \\(dp\\) matrix shown in Figure 14-11, whose size is the same as the input grid \\(grid\\).

    Figure 14-11   State definition and dp table

    Note

    The dynamic programming and backtracking processes can be described as a sequence of decisions, and the state consists of all decision variables. It should contain all variables describing the progress of problem-solving, and should contain sufficient information to derive the next state.

    Each state corresponds to a subproblem, and we define a \\(dp\\) table to store the solutions to all subproblems. Each independent variable of the state is a dimension of the \\(dp\\) table. Essentially, the \\(dp\\) table is a mapping between states and solutions to subproblems.

    Step 2: Identify the optimal substructure, and then derive the state transition equation

    For state \\([i, j]\\), it can only transition from the cell above \\([i-1, j]\\) or the cell to the left \\([i, j-1]\\). Therefore, the optimal substructure is: the minimum path sum to reach \\([i, j]\\) is determined by the smaller of the minimum path sums of \\([i, j-1]\\) and \\([i-1, j]\\).

    Based on the above analysis, the state transition equation shown in Figure 14-12 can be derived:

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

    Figure 14-12   Optimal substructure and state transition equation

    Note

    Based on the defined \\(dp\\) table, think about the relationship between the original problem and subproblems, and find the method to construct the optimal solution to the original problem from the optimal solutions to the subproblems, which is the optimal substructure.

    Once we identify the optimal substructure, we can use it to construct the state transition equation.

    Step 3: Determine boundary conditions and state transition order

    In this problem, states in the first row can only come from the state to their left, and states in the first column can only come from the state above them. Therefore, the first row \\(i = 0\\) and first column \\(j = 0\\) are boundary conditions.

    As shown in Figure 14-13, since each cell transitions from the cell to its left and the cell above it, we use loops to traverse the matrix, with the outer loop traversing rows and the inner loop traversing columns.

    Figure 14-13   Boundary conditions and state transition order

    Note

    Boundary conditions in dynamic programming are used to initialize the \\(dp\\) table, while in search they are used for pruning.

    The core of state transition order is to ensure that when computing the solution to the current problem, all the smaller subproblems it depends on have already been computed correctly.

    Based on the above analysis, we can directly write the dynamic programming code. However, subproblem decomposition is a top-down approach, so implementing in the order \"brute force search \\(\\rightarrow\\) memoization \\(\\rightarrow\\) dynamic programming\" is more aligned with thinking habits.

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-method-1-brute-force-search","level":3,"title":"1.   Method 1: Brute Force Search","text":"

    Starting from state \\([i, j]\\), we continuously decompose it into smaller states \\([i-1, j]\\) and \\([i, j-1]\\). The recursive function includes the following elements.

    • Recursive parameters: state \\([i, j]\\).
    • Return value: minimum path sum from \\([0, 0]\\) to \\([i, j]\\), which is \\(dp[i, j]\\).
    • Termination condition: when \\(i = 0\\) and \\(j = 0\\), return cost \\(grid[0, 0]\\).
    • Pruning: when \\(i < 0\\) or \\(j < 0\\), the index is out of bounds, return cost \\(+\\infty\\), representing infeasibility.

    The implementation code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
    def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n    \"\"\"Minimum path sum: Brute-force search\"\"\"\n    # If it's the top-left cell, terminate the search\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # If row or column index is out of bounds, return +∞ cost\n    if i < 0 or j < 0:\n        return inf\n    # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    up = min_path_sum_dfs(grid, i - 1, j)\n    left = min_path_sum_dfs(grid, i, j - 1)\n    # Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n
    min_path_sum.cpp
    /* Minimum path sum: Brute-force search */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n
    min_path_sum.java
    /* Minimum path sum: Brute-force search */\nint minPathSumDFS(int[][] grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
    min_path_sum.cs
    /* Minimum path sum: Brute-force search */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = MinPathSumDFS(grid, i - 1, j);\n    int left = MinPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.Min(left, up) + grid[i][j];\n}\n
    min_path_sum.go
    /* Minimum path sum: Brute-force search */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    up := minPathSumDFS(grid, i-1, j)\n    left := minPathSumDFS(grid, i, j-1)\n    // Return the minimum path cost from top-left to (i, j)\n    return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n
    min_path_sum.swift
    /* Minimum path sum: Brute-force search */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {\n    // If it's the top-left cell, terminate the search\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (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    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n}\n
    min_path_sum.js
    /* Minimum path sum: Brute-force search */\nfunction minPathSumDFS(grid, i, j) {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
    min_path_sum.ts
    /* Minimum path sum: Brute-force search */\nfunction minPathSumDFS(\n    grid: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
    min_path_sum.dart
    /* Minimum path sum: Brute-force search */\nint minPathSumDFS(List<List<int>> grid, int i, int j) {\n  // If it's the top-left cell, terminate the search\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // If row or column index is out of bounds, return +∞ cost\n  if (i < 0 || j < 0) {\n    // In Dart, int type is fixed-range integer, no value representing \"infinity\"\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n  int up = minPathSumDFS(grid, i - 1, j);\n  int left = minPathSumDFS(grid, i, j - 1);\n  // Return the minimum path cost from top-left to (i, j)\n  return min(left, up) + grid[i][j];\n}\n
    min_path_sum.rs
    /* Minimum path sum: Brute-force search */\nfn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (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    // Return the minimum path cost from top-left to (i, j)\n    std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n
    min_path_sum.c
    /* Minimum path sum: Brute-force search */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n
    min_path_sum.kt
    /* Minimum path sum: Brute-force search */\nfun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    val up = minPathSumDFS(grid, i - 1, j)\n    val left = minPathSumDFS(grid, i, j - 1)\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n}\n
    min_path_sum.rb
    ### Minimum path sum: brute force search ###\ndef min_path_sum_dfs(grid, i, j)\n  # If it's the top-left cell, terminate the search\n  return grid[i][j] if i == 0 && j == 0\n  # If row or column index is out of bounds, return +∞ cost\n  return Float::INFINITY if i < 0 || j < 0\n  # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n  up = min_path_sum_dfs(grid, i - 1, j)\n  left = min_path_sum_dfs(grid, i, j - 1)\n  # Return the minimum path cost from top-left to (i, j)\n  [left, up].min + grid[i][j]\nend\n

    Figure 14-14 shows the recursion tree rooted at \\(dp[2, 1]\\), which includes some overlapping subproblems whose number will increase sharply as the size of grid grid grows.

    Essentially, the reason for overlapping subproblems is: there are multiple paths from the top-left corner to reach a certain cell.

    Figure 14-14   Brute force search recursion tree

    Each state has two choices, down and right, so the total number of steps from the top-left corner to the bottom-right corner is \\(m + n - 2\\), giving a worst-case time complexity of \\(O(2^{m + n})\\), where \\(n\\) and \\(m\\) are the number of rows and columns of the grid, respectively. Note that this calculation does not account for situations near the grid boundaries, where only one choice remains when reaching the grid boundary, so the actual number of paths will be somewhat less.

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-method-2-memoization","level":3,"title":"2.   Method 2: Memoization","text":"

    We introduce a memo list mem of the same size as grid grid to record the solutions to subproblems and prune overlapping subproblems:

    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    \"\"\"Minimum path sum: Memoization search\"\"\"\n    # If it's the top-left cell, terminate the search\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # If row or column index is out of bounds, return +∞ cost\n    if i < 0 or j < 0:\n        return inf\n    # If there's a record, return it directly\n    if mem[i][j] != -1:\n        return mem[i][j]\n    # Minimum path cost for left and upper cells\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    # Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n
    min_path_sum.cpp
    /* Minimum path sum: Memoization search */\nint minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (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
    /* Minimum path sum: Memoization search */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
    min_path_sum.cs
    /* Minimum path sum: Memoization search */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n    int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.Min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
    min_path_sum.go
    /* Minimum path sum: Memoization search */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // If there's a record, return it directly\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    up := minPathSumDFSMem(grid, mem, i-1, j)\n    left := minPathSumDFSMem(grid, mem, i, j-1)\n    // Record and return the minimum path cost from top-left to (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
    /* Minimum path sum: Memoization search */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {\n    // If it's the top-left cell, terminate the search\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // If there's a record, return it directly\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\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    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
    min_path_sum.js
    /* Minimum path sum: Memoization search */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] !== -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
    min_path_sum.ts
    /* Minimum path sum: Memoization search */\nfunction minPathSumDFSMem(\n    grid: Array<Array<number>>,\n    mem: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
    min_path_sum.dart
    /* Minimum path sum: Memoization search */\nint minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {\n  // If it's the top-left cell, terminate the search\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // If row or column index is out of bounds, return +∞ cost\n  if (i < 0 || j < 0) {\n    // In Dart, int type is fixed-range integer, no value representing \"infinity\"\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // If there's a record, return it directly\n  if (mem[i][j] != -1) {\n    return mem[i][j];\n  }\n  // Minimum path cost for left and upper cells\n  int up = minPathSumDFSMem(grid, mem, i - 1, j);\n  int left = minPathSumDFSMem(grid, mem, i, j - 1);\n  // Record and return the minimum path cost from top-left to (i, j)\n  mem[i][j] = min(left, up) + grid[i][j];\n  return mem[i][j];\n}\n
    min_path_sum.rs
    /* Minimum path sum: Memoization search */\nfn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // If there's a record, return it directly\n    if mem[i as usize][j as usize] != -1 {\n        return mem[i as usize][j as usize];\n    }\n    // Minimum path cost for left and upper cells\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    // Record and return the minimum path cost from top-left to (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
    /* Minimum path sum: Memoization search */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (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
    /* Minimum path sum: Memoization search */\nfun minPathSumDFSMem(\n    grid: Array<IntArray>,\n    mem: Array<IntArray>,\n    i: Int,\n    j: Int\n): Int {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    val up = minPathSumDFSMem(grid, mem, i - 1, j)\n    val left = minPathSumDFSMem(grid, mem, i, j - 1)\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
    min_path_sum.rb
    ### Minimum path sum: memoization search ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n  # If it's the top-left cell, terminate the search\n  return grid[0][0] if i == 0 && j == 0\n  # If row or column index is out of bounds, return +∞ cost\n  return Float::INFINITY if i < 0 || j < 0\n  # If there's a record, return it directly\n  return mem[i][j] if mem[i][j] != -1\n  # Minimum path cost for left and upper cells\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  # Record and return the minimum path cost from top-left to (i, j)\n  mem[i][j] = [left, up].min + grid[i][j]\nend\n

    As shown in Figure 14-15, after introducing memoization, all subproblem solutions only need to be computed once, so the time complexity depends on the total number of states, which is the grid size \\(O(nm)\\).

    Figure 14-15   Memoization recursion tree

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-method-3-dynamic-programming","level":3,"title":"3.   Method 3: Dynamic Programming","text":"

    Implement the dynamic programming solution based on iteration, as shown in the code below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
    def min_path_sum_dp(grid: list[list[int]]) -> int:\n    \"\"\"Minimum path sum: Dynamic programming\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Initialize dp table\n    dp = [[0] * m for _ in range(n)]\n    dp[0][0] = grid[0][0]\n    # State transition: first row\n    for j in range(1, m):\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    # State transition: first column\n    for i in range(1, n):\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    # State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint minPathSumDP(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Initialize dp table\n    vector<vector<int>> dp(n, vector<int>(m));\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint minPathSumDP(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Initialize dp table\n    int[][] dp = new int[n][m];\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint MinPathSumDP(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Initialize dp table\n    int[,] dp = new int[n, m];\n    dp[0, 0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0, j] = dp[0, j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfunc minPathSumDP(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Initialize dp table\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    // State transition: first row\n    for j := 1; j < m; j++ {\n        dp[0][j] = dp[0][j-1] + grid[0][j]\n    }\n    // State transition: first column\n    for i := 1; i < n; i++ {\n        dp[i][0] = dp[i-1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfunc minPathSumDP(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for j in 1 ..< m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // State transition: first column\n    for i in 1 ..< n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfunction minPathSumDP(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfunction minPathSumDP(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint minPathSumDP(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));\n  dp[0][0] = grid[0][0];\n  // State transition: first row\n  for (int j = 1; j < m; j++) {\n    dp[0][j] = dp[0][j - 1] + grid[0][j];\n  }\n  // State transition: first column\n  for (int i = 1; i < n; i++) {\n    dp[i][0] = dp[i - 1][0] + grid[i][0];\n  }\n  // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Initialize dp table\n    let mut dp = vec![vec![0; m]; n];\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for j in 1..m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for i in 1..n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Initialize dp table\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    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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    // Free memory\n    for (int i = 0; i < n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
    min_path_sum.kt
    /* Minimum path sum: Dynamic programming */\nfun minPathSumDP(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Initialize dp table\n    val dp = Array(n) { IntArray(m) }\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for (j in 1..<m) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // State transition: first column\n    for (i in 1..<n) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\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
    ### Minimum path sum: dynamic programming ###\ndef min_path_sum_dp(grid)\n  n, m = grid.length, grid.first.length\n  # Initialize dp table\n  dp = Array.new(n) { Array.new(m, 0) }\n  dp[0][0] = grid[0][0]\n  # State transition: first row\n  (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n  # State transition: first column\n  (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n  # State transition: rest of the rows and columns\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

    Figure 14-16 shows the state transition process for minimum path sum, which traverses the entire grid, thus the time complexity is \\(O(nm)\\).

    The array dp has size \\(n \\times m\\), thus the space complexity is \\(O(nm)\\).

    <1><2><3><4><5><6><7><8><9><10><11><12>

    Figure 14-16   Dynamic programming process for minimum path sum

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4-space-optimization","level":3,"title":"4.   Space Optimization","text":"

    Since each cell is only related to the cell to its left and the cell above it, we can use a single-row array to implement the \\(dp\\) table.

    Note that since the array dp can only represent the state of one row, we cannot initialize the first column state in advance, but rather update it when traversing each row:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
    def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n    \"\"\"Minimum path sum: Space-optimized dynamic programming\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Initialize dp table\n    dp = [0] * m\n    # State transition: first row\n    dp[0] = grid[0][0]\n    for j in range(1, m):\n        dp[j] = dp[j - 1] + grid[0][j]\n    # State transition: rest of the rows\n    for i in range(1, n):\n        # State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        # State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Initialize dp table\n    vector<int> dp(m);\n    // State transition: first row\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    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Initialize dp table\n    int[] dp = new int[m];\n    // State transition: first row\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    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint MinPathSumDPComp(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Initialize dp table\n    int[] dp = new int[m];\n    dp[0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfunc minPathSumDPComp(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Initialize dp table\n    dp := make([]int, m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for j := 1; j < m; j++ {\n        dp[j] = dp[j-1] + grid[0][j]\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i < n; i++ {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfunc minPathSumDPComp(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for j in 1 ..< m {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // State transition: rest of the rows\n    for i in 1 ..< n {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfunction minPathSumDPComp(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = new Array(m);\n    // State transition: first row\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    // State transition: rest of the rows\n    for (let i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfunction minPathSumDPComp(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = new Array(m);\n    // State transition: first row\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    // State transition: rest of the rows\n    for (let i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Initialize dp table\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  // State transition: rest of the rows\n  for (int i = 1; i < n; i++) {\n    // State transition: first column\n    dp[0] = dp[0] + grid[i][0];\n    // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Initialize dp table\n    let mut dp = vec![0; m];\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for j in 1..m {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for i in 1..n {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Initialize dp table\n    int *dp = calloc(m, sizeof(int));\n    // State transition: first row\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    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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    // Free memory\n    free(dp);\n    return res;\n}\n
    min_path_sum.kt
    /* Minimum path sum: Space-optimized dynamic programming */\nfun minPathSumDPComp(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Initialize dp table\n    val dp = IntArray(m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for (j in 1..<m) {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // State transition: rest of the rows\n    for (i in 1..<n) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\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
    ### Minimum path sum: space-optimized DP ###\ndef min_path_sum_dp_comp(grid)\n  n, m = grid.length, grid.first.length\n  # Initialize dp table\n  dp = Array.new(m, 0)\n  # State transition: first row\n  dp[0] = grid[0][0]\n  (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n  # State transition: rest of the rows\n  for i in 1...n\n    # State transition: first column\n    dp[0] = dp[0] + grid[i][0]\n    # State transition: rest of the columns\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":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   Edit Distance Problem","text":"

    Edit distance, also known as Levenshtein distance, refers to the minimum number of edits required to transform one string into another, commonly used in information retrieval and natural language processing to measure the similarity between two sequences.

    Question

    Given two strings \\(s\\) and \\(t\\), return the minimum number of edits required to transform \\(s\\) into \\(t\\).

    You can perform three types of edit operations on a string: insert a character, delete a character, or replace a character with any other character.

    As shown in Figure 14-27, transforming kitten into sitting requires 3 edits, including 2 replacements and 1 insertion; transforming hello into algo requires 3 steps, including 2 replacements and 1 deletion.

    Figure 14-27   Example data for edit distance

    The edit distance problem can be naturally explained using the decision tree model. Strings correspond to tree nodes, and each edit operation corresponds to an edge in the tree.

    As shown in Figure 14-28, without restricting operations, each node can branch into many edges, with each edge corresponding to one operation, meaning there are many possible paths to transform hello into algo.

    From the perspective of the decision tree, the goal of this problem is to find the shortest path between node hello and node algo.

    Figure 14-28   Representing edit distance problem based on decision tree model

    ","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1-dynamic-programming-approach","level":3,"title":"1.   Dynamic Programming Approach","text":"

    Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

    Each round of decision involves performing one edit operation on string \\(s\\).

    We want the problem size to gradually decrease during the editing process so that we can construct subproblems. Let the lengths of strings \\(s\\) and \\(t\\) be \\(n\\) and \\(m\\) respectively. We first consider the tail characters of the two strings, \\(s[n-1]\\) and \\(t[m-1]\\).

    • If \\(s[n-1]\\) and \\(t[m-1]\\) are the same, we can skip them and directly consider \\(s[n-2]\\) and \\(t[m-2]\\).
    • If \\(s[n-1]\\) and \\(t[m-1]\\) are different, we need to perform one edit on \\(s\\) (insert, delete, or replace) to make the tail characters of the two strings the same, allowing us to skip them and consider a smaller-scale problem.

    In other words, each round of decision (edit operation) we make on string \\(s\\) will change the remaining characters to be matched in \\(s\\) and \\(t\\). Therefore, the state is the \\(i\\)-th and \\(j\\)-th characters currently being considered in \\(s\\) and \\(t\\), denoted as \\([i, j]\\).

    State \\([i, j]\\) corresponds to the subproblem: the minimum number of edits required to change the first \\(i\\) characters of \\(s\\) into the first \\(j\\) characters of \\(t\\).

    From this, we obtain a two-dimensional \\(dp\\) table of size \\((i+1) \\times (j+1)\\).

    Step 2: Identify the optimal substructure, and then derive the state transition equation

    Consider subproblem \\(dp[i, j]\\), where the tail characters of the corresponding two strings are \\(s[i-1]\\) and \\(t[j-1]\\), which can be divided into the three cases shown in Figure 14-29 based on different edit operations.

    1. Insert \\(t[j-1]\\) after \\(s[i-1]\\), then the remaining subproblem is \\(dp[i, j-1]\\).
    2. Delete \\(s[i-1]\\), then the remaining subproblem is \\(dp[i-1, j]\\).
    3. Replace \\(s[i-1]\\) with \\(t[j-1]\\), then the remaining subproblem is \\(dp[i-1, j-1]\\).

    Figure 14-29   State transition for edit distance

    Based on the above analysis, we obtain the optimal substructure: the minimum number of edits for \\(dp[i, j]\\) equals the minimum of \\(dp[i, j-1]\\), \\(dp[i-1, j]\\), and \\(dp[i-1, j-1]\\), plus the current edit cost of \\(1\\). The corresponding state transition equation is:

    \\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\]

    Please note that when \\(s[i-1]\\) and \\(t[j-1]\\) are the same, no edit is required for the current character, in which case the state transition equation is:

    \\[ dp[i, j] = dp[i-1, j-1] \\]

    Step 3: Determine boundary conditions and state transition order

    When both strings are empty, the number of edit steps is \\(0\\), i.e., \\(dp[0, 0] = 0\\). When \\(s\\) is empty but \\(t\\) is not, the minimum number of edit steps equals the length of \\(t\\), i.e., the first row \\(dp[0, j] = j\\). When \\(s\\) is not empty but \\(t\\) is empty, the minimum number of edit steps equals the length of \\(s\\), i.e., the first column \\(dp[i, 0] = i\\).

    Observing the state transition equation, the solution \\(dp[i, j]\\) depends on solutions to the left, above, and upper-left, so the entire \\(dp\\) table can be traversed in order through two nested loops.

    ","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
    def edit_distance_dp(s: str, t: str) -> int:\n    \"\"\"Edit distance: Dynamic programming\"\"\"\n    n, m = len(s), len(t)\n    dp = [[0] * (m + 1) for _ in range(n + 1)]\n    # State transition: first row and first column\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    # State transition: rest of the rows and columns\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                # If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\nint editDistanceDP(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[][] dp = new int[n + 1][m + 1];\n    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\nint EditDistanceDP(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[,] dp = new int[n + 1, m + 1];\n    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= m; j++ {\n            if s[i-1] == t[j-1] {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\n    for i in 1 ... n {\n        for j in 1 ... m {\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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  // State transition: first row and first column\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  // State transition: rest of the rows and columns\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        // If two characters are equal, skip both characters\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
    edit_distance.kt
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\n    for (i in 1..n) {\n        for (j in 1..m) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    ### Edit distance: dynamic programming ###\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  # State transition: first row and first column\n  (1...(n + 1)).each { |i| dp[i][0] = i }\n  (1...(m + 1)).each { |j| dp[0][j] = j }\n  # State transition: rest of the rows and columns\n  for i in 1...(n + 1)\n    for j in 1...(m +1)\n      if s[i - 1] == t[j - 1]\n        # If two characters are equal, skip both characters\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # Minimum edit steps = minimum edit steps of insert, delete, replace + 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

    As shown in Figure 14-30, the state transition process for the edit distance problem is very similar to that of the knapsack problem; both can be viewed as the process of filling a two-dimensional grid.

    <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

    Figure 14-30   Dynamic programming process for edit distance

    ","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3-space-optimization","level":3,"title":"3.   Space Optimization","text":"

    Since \\(dp[i, j]\\) depends on the states above \\(dp[i-1, j]\\), to the left \\(dp[i, j-1]\\), and at the upper-left \\(dp[i-1, j-1]\\), forward traversal will lose the upper-left state \\(dp[i-1, j-1]\\), while reverse traversal cannot construct \\(dp[i, j-1]\\) in advance, so neither traversal order is suitable.

    For this reason, we can use a variable leftup to temporarily store the upper-left solution \\(dp[i-1, j-1]\\), so we only need to consider the solutions to the left and above. This situation is the same as in the unbounded knapsack problem, so we can use forward traversal. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
    def edit_distance_dp_comp(s: str, t: str) -> int:\n    \"\"\"Edit distance: Space-optimized dynamic programming\"\"\"\n    n, m = len(s), len(t)\n    dp = [0] * (m + 1)\n    # State transition: first row\n    for j in range(1, m + 1):\n        dp[j] = j\n    # State transition: rest of the rows\n    for i in range(1, n + 1):\n        # State transition: first column\n        leftup = dp[0]  # Temporarily store dp[i-1, j-1]\n        dp[0] += 1\n        # State transition: rest of the columns\n        for j in range(1, m + 1):\n            temp = dp[j]\n            if s[i - 1] == t[j - 1]:\n                # If two characters are equal, skip both characters\n                dp[j] = leftup\n            else:\n                # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n            leftup = temp  # Update for next round's dp[i-1, j-1]\n    return dp[m]\n
    edit_distance.cpp
    /* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<int> dp(m + 1, 0);\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.java
    /* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[] dp = new int[m + 1];\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.cs
    /* Edit distance: Space-optimized dynamic programming */\nint EditDistanceDPComp(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[] dp = new int[m + 1];\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.go
    /* Edit distance: Space-optimized dynamic programming */\nfunc editDistanceDPComp(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([]int, m+1)\n    // State transition: first row\n    for j := 1; j <= m; j++ {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for i := 1; i <= n; i++ {\n        // State transition: first column\n        leftUp := dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for j := 1; j <= m; j++ {\n            temp := dp[j]\n            if s[i-1] == t[j-1] {\n                // If two characters are equal, skip both characters\n                dp[j] = leftUp\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n            }\n            leftUp = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
    edit_distance.swift
    /* Edit distance: Space-optimized dynamic programming */\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    // State transition: first row\n    for j in 1 ... m {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for i in 1 ... n {\n        // State transition: first column\n        var leftup = dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for j in 1 ... m {\n            let temp = dp[j]\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
    edit_distance.js
    /* Edit distance: Space-optimized dynamic programming */\nfunction editDistanceDPComp(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // State transition: first row\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i <= n; i++) {\n        // State transition: first column\n        let leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.ts
    /* Edit distance: Space-optimized dynamic programming */\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    // State transition: first row\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i <= n; i++) {\n        // State transition: first column\n        let leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.dart
    /* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(String s, String t) {\n  int n = s.length, m = t.length;\n  List<int> dp = List.filled(m + 1, 0);\n  // State transition: first row\n  for (int j = 1; j <= m; j++) {\n    dp[j] = j;\n  }\n  // State transition: rest of the rows\n  for (int i = 1; i <= n; i++) {\n    // State transition: first column\n    int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n    dp[0] = i;\n    // State transition: rest of the columns\n    for (int j = 1; j <= m; j++) {\n      int temp = dp[j];\n      if (s[i - 1] == t[j - 1]) {\n        // If two characters are equal, skip both characters\n        dp[j] = leftup;\n      } else {\n        // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n      }\n      leftup = temp; // Update for next round's dp[i-1, j-1]\n    }\n  }\n  return dp[m];\n}\n
    edit_distance.rs
    /* Edit distance: Space-optimized dynamic programming */\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    // State transition: first row\n    for j in 1..m {\n        dp[j] = j as i32;\n    }\n    // State transition: rest of the rows\n    for i in 1..=n {\n        // State transition: first column\n        let mut leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i as i32;\n        // State transition: rest of the columns\n        for j in 1..=m {\n            let temp = dp[j];\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    dp[m]\n}\n
    edit_distance.c
    /* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n    int *dp = calloc(m + 1, sizeof(int));\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    int res = dp[m];\n    // Free memory\n    free(dp);\n    return res;\n}\n
    edit_distance.kt
    /* Edit distance: Space-optimized dynamic programming */\nfun editDistanceDPComp(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = IntArray(m + 1)\n    // State transition: first row\n    for (j in 1..m) {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for (i in 1..n) {\n        // State transition: first column\n        var leftup = dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for (j in 1..m) {\n            val temp = dp[j]\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
    edit_distance.rb
    ### Edit distance: space-optimized DP ###\ndef edit_distance_dp_comp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(m + 1, 0)\n  # State transition: first row\n  (1...(m + 1)).each { |j| dp[j] = j }\n  # State transition: rest of the rows\n  for i in 1...(n + 1)\n    # State transition: first column\n    leftup = dp.first # Temporarily store dp[i-1, j-1]\n    dp[0] += 1\n    # State transition: rest of the columns\n    for j in 1...(m + 1)\n      temp = dp[j]\n      if s[i - 1] == t[j - 1]\n        # If two characters are equal, skip both characters\n        dp[j] = leftup\n      else\n        # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n      end\n      leftup = temp # Update for next round's dp[i-1, j-1]\n    end\n  end\n  dp[m]\nend\n
    ","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   Introduction to Dynamic Programming","text":"

    Dynamic programming is an important algorithmic paradigm that decomposes a problem into a series of smaller subproblems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving time efficiency.

    In this section, we start with a classic example, first presenting its brute force backtracking solution, observing the overlapping subproblems within it, and then gradually deriving a more efficient dynamic programming solution.

    Climbing stairs

    Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time, how many different ways are there to reach the top?

    As shown in Figure 14-1, for a \\(3\\)-step staircase, there are \\(3\\) different ways to reach the top.

    Figure 14-1   Number of ways to reach the 3rd step

    The goal of this problem is to determine the number of ways, so we can consider using backtracking to enumerate all possibilities. Specifically, imagine climbing stairs as a multi-round selection process: starting from the ground, choosing to go up \\(1\\) or \\(2\\) steps in each round, incrementing the count by \\(1\\) whenever the top of the stairs is reached, and pruning when exceeding the top. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py
    def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n    \"\"\"Backtracking\"\"\"\n    # When climbing to the n-th stair, add 1 to the solution count\n    if state == n:\n        res[0] += 1\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n:\n            continue\n        # Attempt: make a choice, update state\n        backtrack(choices, state + choice, n, res)\n        # Backtrack\n\ndef climbing_stairs_backtrack(n: int) -> int:\n    \"\"\"Climbing stairs: Backtracking\"\"\"\n    choices = [1, 2]  # Can choose to climb up 1 or 2 stairs\n    state = 0  # Start climbing from the 0-th stair\n    res = [0]  # Use res[0] to record the solution count\n    backtrack(choices, state, n, res)\n    return res[0]\n
    climbing_stairs_backtrack.cpp
    /* Backtracking */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    for (auto &choice : choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    vector<int> choices = {1, 2}; // Can choose to climb up 1 or 2 stairs\n    int state = 0;                // Start climbing from the 0-th stair\n    vector<int> res = {0};        // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res[0];\n}\n
    climbing_stairs_backtrack.java
    /* Backtracking */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (Integer choice : choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    List<Integer> choices = Arrays.asList(1, 2); // Can choose to climb up 1 or 2 stairs\n    int state = 0; // Start climbing from the 0-th stair\n    List<Integer> res = new ArrayList<>();\n    res.add(0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
    climbing_stairs_backtrack.cs
    /* Backtracking */\nvoid Backtrack(List<int> choices, int state, int n, List<int> res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    foreach (int choice in choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        Backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint ClimbingStairsBacktrack(int n) {\n    List<int> choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    int state = 0; // Start climbing from the 0-th stair\n    List<int> res = [0]; // Use res[0] to record the solution count\n    Backtrack(choices, state, n, res);\n    return res[0];\n}\n
    climbing_stairs_backtrack.go
    /* Backtracking */\nfunc backtrack(choices []int, state, n int, res []int) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] = res[0] + 1\n    }\n    // Traverse all choices\n    for _, choice := range choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state+choice > n {\n            continue\n        }\n        // Attempt: make choice, update state\n        backtrack(choices, state+choice, n, res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunc climbingStairsBacktrack(n int) int {\n    // Can choose to climb up 1 or 2 stairs\n    choices := []int{1, 2}\n    // Start climbing from the 0-th stair\n    state := 0\n    res := make([]int, 1)\n    // Use res[0] to record the solution count\n    res[0] = 0\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
    climbing_stairs_backtrack.swift
    /* Backtracking */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] += 1\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n {\n            continue\n        }\n        // Attempt: make choice, update state\n        backtrack(choices: choices, state: state + choice, n: n, res: &res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunc climbingStairsBacktrack(n: Int) -> Int {\n    let choices = [1, 2] // Can choose to climb up 1 or 2 stairs\n    let state = 0 // Start climbing from the 0-th stair\n    var res: [Int] = []\n    res.append(0) // Use res[0] to record the solution count\n    backtrack(choices: choices, state: state, n: n, res: &res)\n    return res[0]\n}\n
    climbing_stairs_backtrack.js
    /* Backtracking */\nfunction backtrack(choices, state, n, res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunction climbingStairsBacktrack(n) {\n    const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    const state = 0; // Start climbing from the 0-th stair\n    const res = new Map();\n    res.set(0, 0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
    climbing_stairs_backtrack.ts
    /* Backtracking */\nfunction backtrack(\n    choices: number[],\n    state: number,\n    n: number,\n    res: Map<0, any>\n): void {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunction climbingStairsBacktrack(n: number): number {\n    const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    const state = 0; // Start climbing from the 0-th stair\n    const res = new Map();\n    res.set(0, 0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
    climbing_stairs_backtrack.dart
    /* Backtracking */\nvoid backtrack(List<int> choices, int state, int n, List<int> res) {\n  // When climbing to the n-th stair, add 1 to the solution count\n  if (state == n) {\n    res[0]++;\n  }\n  // Traverse all choices\n  for (int choice in choices) {\n    // Pruning: not allowed to go beyond the n-th stair\n    if (state + choice > n) continue;\n    // Attempt: make choice, update state\n    backtrack(choices, state + choice, n, res);\n    // Backtrack\n  }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n  List<int> choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n  int state = 0; // Start climbing from the 0-th stair\n  List<int> res = [];\n  res.add(0); // Use res[0] to record the solution count\n  backtrack(choices, state, n, res);\n  return res[0];\n}\n
    climbing_stairs_backtrack.rs
    /* Backtracking */\nfn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] = res[0] + 1;\n    }\n    // Traverse all choices\n    for &choice in choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n {\n            continue;\n        }\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfn climbing_stairs_backtrack(n: usize) -> i32 {\n    let choices = vec![1, 2]; // Can choose to climb up 1 or 2 stairs\n    let state = 0; // Start climbing from the 0-th stair\n    let mut res = Vec::new();\n    res.push(0); // Use res[0] to record the solution count\n    backtrack(&choices, state, n as i32, &mut res);\n    res[0]\n}\n
    climbing_stairs_backtrack.c
    /* Backtracking */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    for (int i = 0; i < len; i++) {\n        int choice = choices[i];\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res, len);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    int choices[2] = {1, 2}; // Can choose to climb up 1 or 2 stairs\n    int state = 0;           // Start climbing from the 0-th stair\n    int *res = (int *)malloc(sizeof(int));\n    *res = 0; // Use res[0] to record the solution count\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
    /* Backtracking */\nfun backtrack(\n    choices: MutableList<Int>,\n    state: Int,\n    n: Int,\n    res: MutableList<Int>\n) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0] = res[0] + 1\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfun climbingStairsBacktrack(n: Int): Int {\n    val choices = mutableListOf(1, 2) // Can choose to climb up 1 or 2 stairs\n    val state = 0 // Start climbing from the 0-th stair\n    val res = mutableListOf<Int>()\n    res.add(0) // Use res[0] to record the solution count\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
    climbing_stairs_backtrack.rb
    ### Backtracking ###\ndef backtrack(choices, state, n, res)\n  # When climbing to the n-th stair, add 1 to the solution count\n  res[0] += 1 if state == n\n  # Traverse all choices\n  for choice in choices\n    # Pruning: not allowed to go beyond the n-th stair\n    next if state + choice > n\n\n    # Attempt: make choice, update state\n    backtrack(choices, state + choice, n, res)\n  end\n  # Backtrack\nend\n\n### Climbing stairs: backtracking ###\ndef climbing_stairs_backtrack(n)\n  choices = [1, 2] # Can choose to climb up 1 or 2 stairs\n  state = 0 # Start climbing from the 0-th stair\n  res = [0] # Use res[0] to record the solution count\n  backtrack(choices, state, n, res)\n  res.first\nend\n
    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-method-1-brute-force-search","level":2,"title":"14.1.1   Method 1: Brute Force Search","text":"

    Backtracking algorithms typically do not explicitly decompose problems, but rather treat solving the problem as a series of decision steps, searching for all possible solutions through trial and pruning.

    We can try to analyze this problem from the perspective of problem decomposition. Let the number of ways to climb to the \\(i\\)-th step be \\(dp[i]\\), then \\(dp[i]\\) is the original problem, and its subproblems include:

    \\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\]

    Since we can only go up \\(1\\) or \\(2\\) steps in each round, when we stand on the \\(i\\)-th step, we could only have been on the \\(i-1\\)-th or \\(i-2\\)-th step in the previous round. In other words, we can only reach the \\(i\\)-th step from the \\(i-1\\)-th or \\(i-2\\)-th step.

    This leads to an important conclusion: the number of ways to climb to the \\(i-1\\)-th step plus the number of ways to climb to the \\(i-2\\)-th step equals the number of ways to climb to the \\(i\\)-th step. The formula is as follows:

    \\[ dp[i] = dp[i-1] + dp[i-2] \\]

    This means that in the stair climbing problem, there exists a recurrence relation among the subproblems, and the solution to the original problem can be constructed from the solutions to the subproblems. Figure 14-2 illustrates this recurrence relation.

    Figure 14-2   Recurrence relation for the number of ways

    We can obtain a brute force search solution based on the recurrence formula. Starting from \\(dp[n]\\), recursively decompose a larger problem into the sum of two smaller problems, until reaching the smallest subproblems \\(dp[1]\\) and \\(dp[2]\\) and returning. Among them, the solutions to the smallest subproblems are known, namely \\(dp[1] = 1\\) and \\(dp[2] = 2\\), representing \\(1\\) and \\(2\\) ways to climb to the \\(1\\)st and \\(2\\)nd steps, respectively.

    Observe the following code: like standard backtracking code, it also uses depth-first search but is more concise:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py
    def dfs(i: int) -> int:\n    \"\"\"Search\"\"\"\n    # Known dp[1] and dp[2], return them\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    \"\"\"Climbing stairs: Search\"\"\"\n    return dfs(n)\n
    climbing_stairs_dfs.cpp
    /* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.java
    /* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.cs
    /* Search */\nint DFS(int i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint ClimbingStairsDFS(int n) {\n    return DFS(n);\n}\n
    climbing_stairs_dfs.go
    /* Search */\nfunc dfs(i int) int {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfunc climbingStairsDFS(n int) int {\n    return dfs(n)\n}\n
    climbing_stairs_dfs.swift
    /* Search */\nfunc dfs(i: Int) -> Int {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfunc climbingStairsDFS(n: Int) -> Int {\n    dfs(i: n)\n}\n
    climbing_stairs_dfs.js
    /* Search */\nfunction dfs(i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfunction climbingStairsDFS(n) {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.ts
    /* Search */\nfunction dfs(i: number): number {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfunction climbingStairsDFS(n: number): number {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.dart
    /* Search */\nint dfs(int i) {\n  // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n  return dfs(n);\n}\n
    climbing_stairs_dfs.rs
    /* Search */\nfn dfs(i: usize) -> i32 {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfn climbing_stairs_dfs(n: usize) -> i32 {\n    dfs(n)\n}\n
    climbing_stairs_dfs.c
    /* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.kt
    /* Search */\nfun dfs(i: Int): Int {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfun climbingStairsDFS(n: Int): Int {\n    return dfs(n)\n}\n
    climbing_stairs_dfs.rb
    ### Search ###\ndef dfs(i)\n  # Known dp[1] and dp[2], return them\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### Climbing stairs: search ###\ndef climbing_stairs_dfs(n)\n  dfs(n)\nend\n

    Figure 14-3 shows the recursion tree formed by brute force search. For the problem \\(dp[n]\\), the depth of its recursion tree is \\(n\\), with a time complexity of \\(O(2^n)\\). Exponential growth is explosive; if we input a relatively large \\(n\\), the wait can be very long.

    Figure 14-3   Recursion tree for climbing stairs

    Observing the above figure, the exponential time complexity is caused by \"overlapping subproblems\". For example, \\(dp[9]\\) is decomposed into \\(dp[8]\\) and \\(dp[7]\\), and \\(dp[8]\\) is decomposed into \\(dp[7]\\) and \\(dp[6]\\), both of which contain the subproblem \\(dp[7]\\).

    And so on, subproblems contain smaller overlapping subproblems, ad infinitum. The vast majority of computational resources are wasted on these overlapping subproblems.

    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-method-2-memoization","level":2,"title":"14.1.2   Method 2: Memoization","text":"

    To improve algorithm efficiency, we want all overlapping subproblems to be computed only once. For this purpose, we declare an array mem to record the solution to each subproblem and prune overlapping subproblems during the search process.

    1. When computing \\(dp[i]\\) for the first time, we record it in mem[i] for later use.
    2. When we need to compute \\(dp[i]\\) again, we can directly retrieve the result from mem[i], thereby avoiding redundant computation of that subproblem.

    The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py
    def dfs(i: int, mem: list[int]) -> int:\n    \"\"\"Memoization search\"\"\"\n    # Known dp[1] and dp[2], return them\n    if i == 1 or i == 2:\n        return i\n    # If record dp[i] exists, return it directly\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    # Record dp[i]\n    mem[i] = count\n    return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n    \"\"\"Climbing stairs: Memoization search\"\"\"\n    # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    mem = [-1] * (n + 1)\n    return dfs(n, mem)\n
    climbing_stairs_dfs_mem.cpp
    /* Memoization search */\nint dfs(int i, vector<int> &mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    vector<int> mem(n + 1, -1);\n    return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.java
    /* Memoization search */\nint dfs(int i, int[] mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int[] mem = new int[n + 1];\n    Arrays.fill(mem, -1);\n    return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.cs
    /* Memoization search */\nint DFS(int i, int[] mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint ClimbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int[] mem = new int[n + 1];\n    Array.Fill(mem, -1);\n    return DFS(n, mem);\n}\n
    climbing_stairs_dfs_mem.go
    /* Memoization search */\nfunc dfsMem(i int, mem []int) int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfunc climbingStairsDFSMem(n int) int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\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
    /* Memoization search */\nfunc dfs(i: Int, mem: inout [Int]) -> Int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfunc climbingStairsDFSMem(n: Int) -> Int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    var mem = Array(repeating: -1, count: n + 1)\n    return dfs(i: n, mem: &mem)\n}\n
    climbing_stairs_dfs_mem.js
    /* Memoization search */\nfunction dfs(i, mem) {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nfunction climbingStairsDFSMem(n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.ts
    /* Memoization search */\nfunction dfs(i: number, mem: number[]): number {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nfunction climbingStairsDFSMem(n: number): number {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.dart
    /* Memoization search */\nint dfs(int i, List<int> mem) {\n  // Known dp[1] and dp[2], return them\n  if (i == 1 || i == 2) return i;\n  // If record dp[i] exists, return it directly\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  // Record dp[i]\n  mem[i] = count;\n  return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n  // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n  List<int> mem = List.filled(n + 1, -1);\n  return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.rs
    /* Memoization search */\nfn dfs(i: usize, mem: &mut [i32]) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    count\n}\n\n/* Climbing stairs: Memoization search */\nfn climbing_stairs_dfs_mem(n: usize) -> i32 {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    let mut mem = vec![-1; n + 1];\n    dfs(n, &mut mem)\n}\n
    climbing_stairs_dfs_mem.c
    /* Memoization search */\nint dfs(int i, int *mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\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
    /* Memoization search */\nfun dfs(i: Int, mem: IntArray): Int {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2) return i\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfun climbingStairsDFSMem(n: Int): Int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    val mem = IntArray(n + 1)\n    mem.fill(-1)\n    return dfs(n, mem)\n}\n
    climbing_stairs_dfs_mem.rb
    ### Memoization search ###\ndef dfs(i, mem)\n  # Known dp[1] and dp[2], return them\n  return i if i == 1 || i == 2\n  # If record dp[i] exists, return it directly\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  # Record dp[i]\n  mem[i] = count\nend\n\n### Climbing stairs: memoization search ###\ndef climbing_stairs_dfs_mem(n)\n  # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n  mem = Array.new(n + 1, -1)\n  dfs(n, mem)\nend\n

    Observe Figure 14-4: after memoization, all overlapping subproblems need to be computed only once, reducing the time complexity to \\(O(n)\\), which is a tremendous leap.

    Figure 14-4   Recursion tree with memoization

    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-method-3-dynamic-programming","level":2,"title":"14.1.3   Method 3: Dynamic Programming","text":"

    Memoization is a \"top-down\" method: we start from the original problem (root node), recursively decompose larger subproblems into smaller ones, until reaching the smallest known subproblems (leaf nodes). Afterward, by backtracking, we collect the solutions to the subproblems layer by layer to construct the solution to the original problem.

    In contrast, dynamic programming is a \"bottom-up\" method: starting from the solutions to the smallest subproblems, iteratively constructing solutions to larger subproblems until obtaining the solution to the original problem.

    Since dynamic programming does not include a backtracking process, it only requires loop iteration for implementation and does not need recursion. In the following code, we initialize an array dp to store the solutions to subproblems, which serves the same recording function as the array mem in memoization:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
    def climbing_stairs_dp(n: int) -> int:\n    \"\"\"Climbing stairs: Dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return n\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [0] * (n + 1)\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1], dp[2] = 1, 2\n    # State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    vector<int> dp(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint ClimbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfunc climbingStairsDP(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfunc climbingStairsDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: 0, count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfunction climbingStairsDP(n) {\n    if (n === 1 || n === 2) return n;\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1).fill(-1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfunction climbingStairsDP(n: number): number {\n    if (n === 1 || n === 2) return n;\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1).fill(-1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n  if (n == 1 || n == 2) return n;\n  // Initialize dp table, used to store solutions to subproblems\n  List<int> dp = List.filled(n + 1, 0);\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1] = 1;\n  dp[2] = 2;\n  // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfn climbing_stairs_dp(n: usize) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![-1; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int *dp = (int *)malloc((n + 1) * sizeof(int));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfun climbingStairsDP(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = IntArray(n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\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
    ### Climbing stairs: dynamic programming ###\ndef climbing_stairs_dp(n)\n  return n  if n == 1 || n == 2\n\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = 1, 2\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n  dp[n]\nend\n

    Figure 14-5 simulates the execution process of the above code.

    Figure 14-5   Dynamic programming process for climbing stairs

    Like backtracking algorithms, dynamic programming also uses the \"state\" concept to represent specific stages of problem solving, with each state corresponding to a subproblem and its corresponding local optimal solution. For example, the state in the stair climbing problem is defined as the current stair step number \\(i\\).

    Based on the above content, we can summarize the commonly used terminology in dynamic programming.

    • The array dp is called the dp table, where \\(dp[i]\\) represents the solution to the subproblem corresponding to state \\(i\\).
    • The states corresponding to the smallest subproblems (the \\(1\\)st and \\(2\\)nd steps) are called initial states.
    • The recurrence formula \\(dp[i] = dp[i-1] + dp[i-2]\\) is called the state transition equation.
    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414-space-optimization","level":2,"title":"14.1.4   Space Optimization","text":"

    Observant readers may have noticed that since \\(dp[i]\\) is only related to \\(dp[i-1]\\) and \\(dp[i-2]\\), we do not need to use an array dp to store the solutions to all subproblems, and can instead use two variables that roll forward. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
    def climbing_stairs_dp_comp(n: int) -> int:\n    \"\"\"Climbing stairs: Space-optimized dynamic programming\"\"\"\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\nfunc climbingStairsDPComp(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    a, b := 1, 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        a, b = b, a+b\n    }\n    return b\n}\n
    climbing_stairs_dp.swift
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    ### Climbing stairs: space-optimized DP ###\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

    As the above code shows, by eliminating the space occupied by the array dp, the space complexity is reduced from \\(O(n)\\) to \\(O(1)\\).

    In dynamic programming problems, the current state often depends only on a limited number of preceding states, allowing us to retain only the necessary states and save memory space through \"dimension reduction\". This space optimization technique is called \"rolling variable\" or \"rolling array\".

    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   0-1 Knapsack Problem","text":"

    The knapsack problem is an excellent introductory problem for dynamic programming and is one of the most common problem forms in dynamic programming. It has many variants, such as the 0-1 knapsack problem, the unbounded knapsack problem, and the multiple knapsack problem.

    In this section, we will first solve the most common 0-1 knapsack problem.

    Question

    Given \\(n\\) items and a knapsack with capacity \\(cap\\), where the weight and value of the \\(i\\)-th item are \\(wgt[i-1]\\) and \\(val[i-1]\\), respectively. Each item can be selected at most once. What is the maximum value that can fit in the knapsack under the capacity limit?

    Observe Figure 14-17. Since item number \\(i\\) starts counting from \\(1\\) and array indices start from \\(0\\), item \\(i\\) corresponds to weight \\(wgt[i-1]\\) and value \\(val[i-1]\\).

    Figure 14-17   Example data for 0-1 knapsack

    We can view the 0-1 knapsack problem as a process consisting of \\(n\\) rounds of decisions, where for each item there are two decisions: not putting it in and putting it in, thus the problem satisfies the decision tree model.

    The goal of this problem is to find \"the maximum value that can be placed in the knapsack within the capacity limit\", so it is more likely to be a dynamic programming problem.

    Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

    For each item, if not placed in the knapsack, the knapsack capacity remains unchanged; if placed in, the knapsack capacity decreases. From this, we can derive the state definition: current item number \\(i\\) and knapsack capacity \\(c\\), denoted as \\([i, c]\\).

    State \\([i, c]\\) corresponds to the subproblem: the maximum value among the first \\(i\\) items in a knapsack of capacity \\(c\\), denoted as \\(dp[i, c]\\).

    What we need to find is \\(dp[n, cap]\\), so we need a two-dimensional \\(dp\\) table of size \\((n+1) \\times (cap+1)\\).

    Step 2: Identify the optimal substructure, and then derive the state transition equation

    After making the decision for item \\(i\\), what remains is the subproblem of the first \\(i-1\\) items, which can be divided into the following two cases.

    • Not putting item \\(i\\): The knapsack capacity remains unchanged, and the state changes to \\([i-1, c]\\).
    • Putting item \\(i\\): The knapsack capacity decreases by \\(wgt[i-1]\\), the value increases by \\(val[i-1]\\), and the state changes to \\([i-1, c-wgt[i-1]]\\).

    The above analysis reveals the optimal substructure of this problem: the maximum value \\(dp[i, c]\\) equals the greater of the values obtained by not putting item \\(i\\) into the knapsack and by putting it into the knapsack. From this, the state transition equation can be derived:

    \\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\]

    Note that if the weight of the current item \\(wgt[i - 1]\\) exceeds the remaining knapsack capacity \\(c\\), then the only option is not to put it in the knapsack.

    Step 3: Determine boundary conditions and state transition order

    When there are no items or the knapsack capacity is \\(0\\), the maximum value is \\(0\\), i.e., the first column \\(dp[i, 0]\\) and the first row \\(dp[0, c]\\) are both equal to \\(0\\).

    The current state \\([i, c]\\) transitions from the state above \\([i-1, c]\\) and the upper-left state \\([i-1, c-wgt[i-1]]\\), so we can traverse the entire \\(dp\\) table in forward order using two nested loops.

    Based on the above analysis, we will next implement the brute force search, memoization, and dynamic programming solutions in order.

    ","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-method-1-brute-force-search","level":3,"title":"1.   Method 1: Brute Force Search","text":"

    The search code includes the following elements.

    • Recursive parameters: state \\([i, c]\\).
    • Return value: solution to the subproblem \\(dp[i, c]\\).
    • Termination condition: when there are no items left (\\(i = 0\\)) or the remaining knapsack capacity is \\(0\\), terminate the recursion and return value \\(0\\).
    • Pruning: if the weight of the current item exceeds the remaining knapsack capacity, only the option of not putting it in is available.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
    def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n    \"\"\"0-1 knapsack: Brute-force search\"\"\"\n    # If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 or c == 0:\n        return 0\n    # If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c:\n        return knapsack_dfs(wgt, val, i - 1, c)\n    # Calculate the maximum value of not putting in and putting in item 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    # Return the larger value of the two options\n    return max(no, yes)\n
    knapsack.cpp
    /* 0-1 knapsack: Brute-force search */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return max(no, yes);\n}\n
    knapsack.java
    /* 0-1 knapsack: Brute-force search */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
    knapsack.cs
    /* 0-1 knapsack: Brute-force search */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (weight[i - 1] > c) {\n        return KnapsackDFS(weight, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return Math.Max(no, yes);\n}\n
    knapsack.go
    /* 0-1 knapsack: Brute-force search */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i-1] > c {\n        return knapsackDFS(wgt, val, i-1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    no := knapsackDFS(wgt, val, i-1, c)\n    yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n    // Return the larger value of the two options\n    return int(math.Max(float64(no), float64(yes)))\n}\n
    knapsack.swift
    /* 0-1 knapsack: Brute-force search */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c {\n        return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return max(no, yes)\n}\n
    knapsack.js
    /* 0-1 knapsack: Brute-force search */\nfunction knapsackDFS(wgt, val, i, c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
    knapsack.ts
    /* 0-1 knapsack: Brute-force search */\nfunction knapsackDFS(\n    wgt: Array<number>,\n    val: Array<number>,\n    i: number,\n    c: number\n): number {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
    knapsack.dart
    /* 0-1 knapsack: Brute-force search */\nint knapsackDFS(List<int> wgt, List<int> val, int i, int c) {\n  // If all items have been selected or knapsack has no remaining capacity, return value 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // If exceeds knapsack capacity, can only choose not to put it in\n  if (wgt[i - 1] > c) {\n    return knapsackDFS(wgt, val, i - 1, c);\n  }\n  // Calculate the maximum value of not putting in and putting in item 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  // Return the larger value of the two options\n  return max(no, yes);\n}\n
    knapsack.rs
    /* 0-1 knapsack: Brute-force search */\nfn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    std::cmp::max(no, yes)\n}\n
    knapsack.c
    /* 0-1 knapsack: Brute-force search */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return myMax(no, yes);\n}\n
    knapsack.kt
    /* 0-1 knapsack: Brute-force search */\nfun knapsackDFS(\n    wgt: IntArray,\n    _val: IntArray,\n    i: Int,\n    c: Int\n): Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, _val, i - 1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return max(no, yes)\n}\n
    knapsack.rb
    ### 0-1 knapsack: brute force search ###\ndef knapsack_dfs(wgt, val, i, c)\n  # If all items have been selected or knapsack has no remaining capacity, return value 0\n  return 0 if i == 0 || c == 0\n  # If exceeds knapsack capacity, can only choose not to put it in\n  return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c\n  # Calculate the maximum value of not putting in and putting in item 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  # Return the larger value of the two options\n  [no, yes].max\nend\n

    As shown in Figure 14-18, since each item generates two search branches, excluding it and including it, the time complexity is \\(O(2^n)\\).

    Observing the recursion tree, it is easy to see overlapping subproblems, such as \\(dp[1, 10]\\). When there are many items, large knapsack capacity, and especially many items with the same weight, the number of overlapping subproblems will increase significantly.

    Figure 14-18   Brute force search recursion tree for 0-1 knapsack problem

    ","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-method-2-memoization","level":3,"title":"2.   Method 2: Memoization","text":"

    To ensure that overlapping subproblems are only computed once, we use a memo list mem to record the solutions to subproblems, where mem[i][c] corresponds to \\(dp[i, c]\\).

    After introducing memoization, the time complexity depends on the number of subproblems, which is \\(O(n \\times cap)\\). The implementation code is as follows:

    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 knapsack: Memoization search\"\"\"\n    # If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 or c == 0:\n        return 0\n    # If there's a record, return it directly\n    if mem[i][c] != -1:\n        return mem[i][c]\n    # If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c:\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    # Calculate the maximum value of not putting in and putting in item 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    # Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n
    knapsack.cpp
    /* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.java
    /* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.cs
    /* 0-1 knapsack: Memoization search */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (weight[i - 1] > c) {\n        return KnapsackDFSMem(weight, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = Math.Max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.go
    /* 0-1 knapsack: Memoization search */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i-1] > c {\n        return knapsackDFSMem(wgt, val, mem, i-1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    mem[i][c] = int(math.Max(float64(no), float64(yes)))\n    return mem[i][c]\n}\n
    knapsack.swift
    /* 0-1 knapsack: Memoization search */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c {\n        return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
    knapsack.js
    /* 0-1 knapsack: Memoization search */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.ts
    /* 0-1 knapsack: Memoization search */\nfunction knapsackDFSMem(\n    wgt: Array<number>,\n    val: Array<number>,\n    mem: Array<Array<number>>,\n    i: number,\n    c: number\n): number {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.dart
    /* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(\n  List<int> wgt,\n  List<int> val,\n  List<List<int>> mem,\n  int i,\n  int c,\n) {\n  // If all items have been selected or knapsack has no remaining capacity, return value 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // If there's a record, return it directly\n  if (mem[i][c] != -1) {\n    return mem[i][c];\n  }\n  // If exceeds knapsack capacity, can only choose not to put it in\n  if (wgt[i - 1] > c) {\n    return knapsackDFSMem(wgt, val, mem, i - 1, c);\n  }\n  // Calculate the maximum value of not putting in and putting in item 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  // Record and return the larger value of the two options\n  mem[i][c] = max(no, yes);\n  return mem[i][c];\n}\n
    knapsack.rs
    /* 0-1 knapsack: Memoization search */\nfn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = std::cmp::max(no, yes);\n    mem[i][c]\n}\n
    knapsack.c
    /* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = myMax(no, yes);\n    return mem[i][c];\n}\n
    knapsack.kt
    /* 0-1 knapsack: Memoization search */\nfun knapsackDFSMem(\n    wgt: IntArray,\n    _val: IntArray,\n    mem: Array<IntArray>,\n    i: Int,\n    c: Int\n): Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
    knapsack.rb
    ### 0-1 knapsack: memoization search ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n  # If all items have been selected or knapsack has no remaining capacity, return value 0\n  return 0 if i == 0 || c == 0\n  # If there's a record, return it directly\n  return mem[i][c] if mem[i][c] != -1\n  # If exceeds knapsack capacity, can only choose not to put it in\n  return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c\n  # Calculate the maximum value of not putting in and putting in item 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  # Record and return the larger value of the two options\n  mem[i][c] = [no, yes].max\nend\n

    Figure 14-19 shows the search branches pruned in memoization.

    Figure 14-19   Memoization recursion tree for 0-1 knapsack problem

    ","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-method-3-dynamic-programming","level":3,"title":"3.   Method 3: Dynamic Programming","text":"

    Dynamic programming is essentially the process of filling the \\(dp\\) table during state transitions. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
    def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 knapsack: Dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # State transition\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (weight[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfunc knapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfunction knapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(n + 1)\n        .fill(0)\n        .map(() => Array(cap + 1).fill(0));\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfunction knapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint knapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\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    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
    knapsack.kt
    /* 0-1 knapsack: Dynamic programming */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: dynamic programming ###\ndef knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # State transition\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # The larger value between not selecting and selecting item 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

    As shown in Figure 14-20, both time complexity and space complexity are determined by the size of the array dp, which is \\(O(n \\times cap)\\).

    <1><2><3><4><5><6><7><8><9><10><11><12><13><14>

    Figure 14-20   Dynamic programming process for 0-1 knapsack problem

    ","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4-space-optimization","level":3,"title":"4.   Space Optimization","text":"

    Since each state is only related to the state in the row above it, we can use two arrays rolling forward to reduce the space complexity from \\(O(n^2)\\) to \\(O(n)\\).

    Further thinking, can we achieve space optimization using just one array? Observing, we can see that each state is transferred from the cell directly above or the cell in the upper-left. If there is only one array, when we start traversing row \\(i\\), that array still stores the state of row \\(i-1\\).

    • If using forward traversal, then when traversing to \\(dp[i, j]\\), the values in the upper-left \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) may have already been overwritten, thus preventing correct state transition.
    • If using reverse traversal, there will be no overwriting issue, and state transition can proceed correctly.

    Figure 14-21 shows the transition process from row \\(i = 1\\) to row \\(i = 2\\) using a single array. Please consider the difference between forward and reverse traversal.

    <1><2><3><4><5><6>

    Figure 14-21   Space-optimized dynamic programming process for 0-1 knapsack

    In the code implementation, we simply need to delete the first dimension \\(i\\) of the array dp and change the inner loop to reverse traversal:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
    def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 knapsack: Space-optimized dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [0] * (cap + 1)\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in reverse order\n        for c in range(cap, 0, -1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
    knapsack.cpp
    /* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<int> dp(cap + 1, 0);\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c > 0; c--) {\n            if (weight[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([]int, cap+1)\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in reverse order\n        for c := cap; c >= 1; c-- {\n            if wgt[i-1] <= c {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: cap + 1)\n    // State transition\n    for i in 1 ... n {\n        // Traverse in reverse order\n        for c in (1 ... cap).reversed() {\n            if wgt[i - 1] <= c {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfunction knapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(cap + 1).fill(0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfunction knapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(cap + 1).fill(0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<int> dp = List.filled(cap + 1, 0);\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    // Traverse in reverse order\n    for (int c = cap; c >= 1; c--) {\n      if (wgt[i - 1] <= c) {\n        // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![0; cap + 1];\n    // State transition\n    for i in 1..=n {\n        // Traverse in reverse order\n        for c in (1..=cap).rev() {\n            if wgt[i - 1] <= c as i32 {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int *dp = calloc(cap + 1, sizeof(int));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Free memory\n    free(dp);\n    return res;\n}\n
    knapsack.kt
    /* 0-1 knapsack: Space-optimized dynamic programming */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = IntArray(cap + 1)\n    // State transition\n    for (i in 1..n) {\n        // Traverse in reverse order\n        for (c in cap downTo 1) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: space-optimized DP ###\ndef knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(cap + 1, 0)\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in reverse order\n    for c in cap.downto(1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c]\n      else\n        # The larger value between not selecting and selecting item 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":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   Summary","text":"","path":["Chapter 14. Dynamic Programming","14.7   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1-key-points","level":3,"title":"1.   Key Points","text":"
    • Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency.
    • Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once.
    • Memoization is a top-down recursive solution, while the corresponding dynamic programming is a bottom-up iterative solution, similar to \"filling in a table\". Since the current state only depends on certain local states, we can eliminate one dimension of the \\(dp\\) table to reduce space complexity.
    • Subproblem decomposition is a general algorithmic approach, with different properties in divide and conquer, dynamic programming, and backtracking.
    • Dynamic programming problems have three major characteristics: overlapping subproblems, optimal substructure, and no aftereffects.
    • If the optimal solution to the original problem can be constructed from the optimal solutions to the subproblems, then it has optimal substructure.
    • No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not satisfy this property and cannot be solved efficiently using dynamic programming.

    Knapsack problem

    • The knapsack problem is one of the most typical dynamic programming problems, with variants such as the 0-1 knapsack, unbounded knapsack, and multiple knapsack.
    • The state definition for the 0-1 knapsack is the maximum value achievable using the first \\(i\\) items with a knapsack capacity of \\(c\\). Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state.
    • The unbounded knapsack problem has no limit on the selection quantity of each type of item, so the state transition for choosing to put in an item differs from the 0-1 knapsack problem. Since the state depends on the state directly above and directly to the left, space optimization should use forward traversal.
    • The coin change problem is a variant of the unbounded knapsack problem. It changes from seeking the \"maximum\" value to seeking the \"minimum\" number of coins, so \\(\\max()\\) in the state transition equation should be changed to \\(\\min()\\). It changes from seeking \"not exceeding\" the knapsack capacity to seeking \"exactly\" making up the target amount, so \\(amt + 1\\) is used to represent the invalid solution of \"unable to make up the target amount\".
    • Coin change problem II changes from seeking the \"minimum number of coins\" to seeking the \"number of coin combinations\", so the state transition equation correspondingly changes from \\(\\min()\\) to a summation operator.

    Edit distance problem

    • Edit distance (Levenshtein distance) is used to measure the similarity between two strings, defined as the minimum number of edit steps from one string to another, with edit operations including insert, delete, and replace.
    • The state definition for the edit distance problem is the minimum number of edit steps required to change the first \\(i\\) characters of \\(s\\) into the first \\(j\\) characters of \\(t\\). When \\(s[i] \\ne t[j]\\), there are three decisions: insert, delete, replace, each with corresponding remaining subproblems. From this, the optimal substructure can be identified and the state transition equation constructed. When \\(s[i] = t[j]\\), no edit is required for the current character.
    • In edit distance, the state depends on the state directly above, directly to the left, and to the upper-left, so after space optimization, neither forward nor reverse traversal can correctly perform state transitions. For this reason, we use a variable to temporarily store the upper-left state, thus transforming to a situation equivalent to the unbounded knapsack problem, allowing for forward traversal after space optimization.
    ","path":["Chapter 14. Dynamic Programming","14.7   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   Unbounded Knapsack Problem","text":"

    In this section, we first solve another common knapsack problem: the unbounded knapsack, and then explore a special case of it: the coin change problem.

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451-unbounded-knapsack-problem","level":2,"title":"14.5.1   Unbounded Knapsack Problem","text":"

    Question

    Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can be selected multiple times. What is the maximum value that can be placed in the knapsack within the capacity limit? An example is shown in Figure 14-22.

    Figure 14-22   Example data for unbounded knapsack problem

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach","level":3,"title":"1.   Dynamic Programming Approach","text":"

    The unbounded knapsack problem is very similar to the 0-1 knapsack problem, differing only in that there is no limit on the number of times an item can be selected.

    • In the 0-1 knapsack problem, there is only one of each type of item, so after placing item \\(i\\) in the knapsack, we can only choose from the first \\(i-1\\) items.
    • In the unbounded knapsack problem, the quantity of each type of item is unlimited, so after placing item \\(i\\) in the knapsack, we can still choose from the first \\(i\\) items.

    Under the rules of the unbounded knapsack problem, the changes in state \\([i, c]\\) are divided into two cases.

    • Not putting item \\(i\\): Same as the 0-1 knapsack problem, transfer to \\([i-1, c]\\).
    • Putting item \\(i\\): Different from the 0-1 knapsack problem, transfer to \\([i, c-wgt[i-1]]\\).

    Thus, the state transition equation becomes:

    \\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    Comparing the code for the two problems, there is one change in state transition from \\(i-1\\) to \\(i\\), with everything else identical:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
    def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Unbounded knapsack: Dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # State transition\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfunction unboundedKnapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\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    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
    unbounded_knapsack.kt
    /* Unbounded knapsack: Dynamic programming */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    ### Unbounded knapsack: dynamic programming ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # State transition\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # The larger value between not selecting and selecting item 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":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization","level":3,"title":"3.   Space Optimization","text":"

    Since the current state is transferred from states on the left and above, after space optimization, each row in the \\(dp\\) table should be traversed in forward order.

    This traversal order is exactly opposite to the 0-1 knapsack. Please refer to Figure 14-23 to understand the difference between the two.

    <1><2><3><4><5><6>

    Figure 14-23   Space-optimized dynamic programming process for unbounded knapsack problem

    The code implementation is relatively simple, just delete the first dimension of the array dp:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
    def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Unbounded knapsack: Space-optimized dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [0] * (cap + 1)\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
    unbounded_knapsack.cpp
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<int> dp(cap + 1, 0);\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([]int, cap+1)\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: cap + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfunction unboundedKnapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<int> dp = List.filled(cap + 1, 0);\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c];\n      } else {\n        // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![0; cap + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int *dp = calloc(cap + 1, sizeof(int));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Free memory\n    free(dp);\n    return res;\n}\n
    unbounded_knapsack.kt
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfun unboundedKnapsackDPComp(\n    wgt: IntArray,\n    _val: IntArray,\n    cap: Int\n): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = IntArray(cap + 1)\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    ### Unbounded knapsack: space-optimized DP ###\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(cap + 1, 0)\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for c in 1...(cap + 1)\n      if wgt[i -1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c]\n      else\n        # The larger value between not selecting and selecting item 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":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452-coin-change-problem","level":2,"title":"14.5.2   Coin Change Problem","text":"

    The knapsack problem represents a large class of dynamic programming problems and has many variants, such as the coin change problem.

    Question

    Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\). Each type of coin can be selected multiple times. What is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return \\(-1\\). An example is shown in Figure 14-24.

    Figure 14-24   Example data for coin change problem

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach_1","level":3,"title":"1.   Dynamic Programming Approach","text":"

    The coin change problem can be viewed as a special case of the unbounded knapsack problem, with the following connections and differences.

    • The two problems can be converted to each other: \"item\" corresponds to \"coin\", \"item weight\" corresponds to \"coin denomination\", and \"knapsack capacity\" corresponds to \"target amount\".
    • The optimization goals are opposite: the unbounded knapsack problem aims to maximize item value, while the coin change problem aims to minimize the number of coins.
    • The unbounded knapsack problem seeks solutions \"not exceeding\" the knapsack capacity, while the coin change problem seeks solutions that \"exactly\" make up the target amount.

    Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

    State \\([i, a]\\) corresponds to the subproblem: the minimum number of coins among the first \\(i\\) types of coins that can make up amount \\(a\\), denoted as \\(dp[i, a]\\).

    The two-dimensional \\(dp\\) table has size \\((n+1) \\times (amt+1)\\).

    Step 2: Identify the optimal substructure, and then derive the state transition equation

    This problem differs from the unbounded knapsack problem in the following two aspects regarding the state transition equation.

    • This problem seeks the minimum value, so the operator \\(\\max()\\) needs to be changed to \\(\\min()\\).
    • The optimization target is the number of coins rather than item value, so when a coin is selected, simply add \\(1\\).
    \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

    Step 3: Determine boundary conditions and state transition order

    When the target amount is \\(0\\), the minimum number of coins needed to make it up is \\(0\\), so all \\(dp[i, 0]\\) in the first column equal \\(0\\).

    When there are no coins, it is impossible to make up any amount \\(> 0\\), which is an invalid solution. To enable the \\(\\min()\\) function in the state transition equation to identify and filter out invalid solutions, we consider using \\(+ \\infty\\) to represent them, i.e., set all \\(dp[0, a]\\) in the first row to \\(+ \\infty\\).

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation_1","level":3,"title":"2.   Code Implementation","text":"

    Most programming languages do not provide a \\(+ \\infty\\) variable, and can only use the maximum value of integer type int as a substitute. However, this can lead to integer overflow: the \\(+ 1\\) operation in the state transition equation may cause overflow.

    For this reason, we use the number \\(amt + 1\\) to represent invalid solutions, because the maximum number of coins needed to make up \\(amt\\) is at most \\(amt\\). Before returning, check whether \\(dp[n, amt]\\) equals \\(amt + 1\\); if so, return \\(-1\\), indicating that the target amount cannot be made up. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
    def coin_change_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Dynamic programming\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Initialize dp table\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # State transition: first row and first column\n    for a in range(1, amt + 1):\n        dp[0][a] = MAX\n    # State transition: rest of the rows and columns\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint coinChangeDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint coinChangeDP(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][amt + 1];\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint CoinChangeDP(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, amt + 1];\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0, a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunc coinChangeDP(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // State transition: first row and first column\n    for a := 1; a <= amt; a++ {\n        dp[0][a] = max\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunc coinChangeDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // State transition: first row and first column\n    for a in 1 ... amt {\n        dp[0][a] = MAX\n    }\n    // State transition: rest of the rows and columns\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunction coinChangeDP(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunction coinChangeDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint coinChangeDP(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // State transition: first row and first column\n  for (int a = 1; a <= amt; a++) {\n    dp[0][a] = MAX;\n  }\n  // State transition: rest of the rows and columns\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfn coin_change_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Initialize dp table\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // State transition: first row and first column\n    for a in 1..=amt {\n        dp[0][a] = max;\n    }\n    // State transition: rest of the rows and columns\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Initialize dp table\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    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
    coin_change.kt
    /* Coin change: Dynamic programming */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // State transition: first row and first column\n    for (a in 1..amt) {\n        dp[0][a] = MAX\n    }\n    // State transition: rest of the rows and columns\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    ### Coin change: dynamic programming ###\ndef coin_change_dp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # State transition: first row and first column\n  (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n  # State transition: rest of the rows and columns\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # The smaller value between not selecting and selecting coin 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

    Figure 14-25 shows the dynamic programming process for coin change, which is very similar to the unbounded knapsack problem.

    <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

    Figure 14-25   Dynamic programming process for coin change problem

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization_1","level":3,"title":"3.   Space Optimization","text":"

    The space optimization for the coin change problem is handled in the same way as the unbounded knapsack problem:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
    def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Space-optimized dynamic programming\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Initialize dp table\n    dp = [MAX] * (amt + 1)\n    dp[0] = 0\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            else:\n                # The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Initialize dp table\n    vector<int> dp(amt + 1, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    Arrays.fill(dp, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint CoinChangeDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    Array.Fill(dp, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunc coinChangeDPComp(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Initialize dp table\n    dp := make([]int, amt+1)\n    for i := 1; i <= amt; i++ {\n        dp[i] = max\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in forward order\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Initialize dp table\n    var dp = Array(repeating: MAX, count: amt + 1)\n    dp[0] = 0\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nfunction coinChangeDPComp(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nfunction coinChangeDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Initialize dp table\n  List<int> dp = List.filled(amt + 1, MAX);\n  dp[0] = 0;\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[a] = dp[a];\n      } else {\n        // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nfn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Initialize dp table\n    let mut dp = vec![0; amt + 1];\n    dp.fill(max);\n    dp[0] = 0;\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Initialize dp table\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    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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    // Free memory\n    free(dp);\n    return res;\n}\n
    coin_change.kt
    /* Coin change: Space-optimized dynamic programming */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Initialize dp table\n    val dp = IntArray(amt + 1)\n    dp.fill(MAX)\n    dp[0] = 0\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    ### Coin change: space-optimized DP ###\ndef coin_change_dp_comp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Initialize dp table\n  dp = Array.new(amt + 1, _MAX)\n  dp[0] = 0\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[a] = dp[a]\n      else\n        # The smaller value between not selecting and selecting coin 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":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-coin-change-problem-ii","level":2,"title":"14.5.3   Coin Change Problem II","text":"

    Question

    Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\). Each type of coin can be selected multiple times. What is the number of coin combinations that can make up the target amount? An example is shown in Figure 14-26.

    Figure 14-26   Example data for coin change problem II

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach_2","level":3,"title":"1.   Dynamic Programming Approach","text":"

    Compared to the previous problem, this problem's goal is to find the number of combinations, so the subproblem becomes: the number of combinations among the first \\(i\\) types of coins that can make up amount \\(a\\). The \\(dp\\) table remains a two-dimensional matrix of size \\((n+1) \\times (amt + 1)\\).

    The number of combinations for the current state equals the sum of the combinations from not selecting the current coin and selecting the current coin. The state transition equation is:

    \\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

    When the target amount is \\(0\\), no coins need to be selected to make up the target amount, so all \\(dp[i, 0]\\) in the first column should be initialized to \\(1\\). When there are no coins, it is impossible to make up any amount \\(>0\\), so all \\(dp[0, a]\\) in the first row equal \\(0\\).

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation_2","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
    def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change II: Dynamic programming\"\"\"\n    n = len(coins)\n    # Initialize dp table\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # Initialize first column\n    for i in range(n + 1):\n        dp[i][0] = 1\n    # State transition\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint coinChangeIIDP(int[] coins, int amt) {\n    int n = coins.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][amt + 1];\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint CoinChangeIIDP(int[] coins, int amt) {\n    int n = coins.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, amt + 1];\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i, 0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfunc coinChangeIIDP(coins []int, amt int) int {\n    n := len(coins)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // Initialize first column\n    for i := 0; i <= n; i++ {\n        dp[i][0] = 1\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // Initialize first column\n    for i in 0 ... n {\n        dp[i][0] = 1\n    }\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfunction coinChangeIIDP(coins, amt) {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Initialize first column\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfunction coinChangeIIDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Initialize first column\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint coinChangeIIDP(List<int> coins, int amt) {\n  int n = coins.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // Initialize first column\n  for (int i = 0; i <= n; i++) {\n    dp[i][0] = 1;\n  }\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // Initialize first column\n    for i in 0..=n {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Initialize dp table\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    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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    // Free memory\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
    /* Coin change II: Dynamic programming */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // Initialize first column\n    for (i in 0..n) {\n        dp[i][0] = 1\n    }\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    ### Coin change II: dynamic programming ###\ndef coin_change_ii_dp(coins, amt)\n  n = coins.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # Initialize first column\n  (0...(n + 1)).each { |i| dp[i][0] = 1 }\n  # State transition\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # Sum of the two options: not selecting and selecting coin 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":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization_2","level":3,"title":"3.   Space Optimization","text":"

    The space optimization is handled in the same way, just delete the coin dimension:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
    def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change II: Space-optimized dynamic programming\"\"\"\n    n = len(coins)\n    # Initialize dp table\n    dp = [0] * (amt + 1)\n    dp[0] = 1\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            else:\n                # Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n    return dp[amt]\n
    coin_change_ii.cpp
    /* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Initialize dp table\n    vector<int> dp(amt + 1, 0);\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.java
    /* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.cs
    /* Coin change II: Space-optimized dynamic programming */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.go
    /* Coin change II: Space-optimized dynamic programming */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n    n := len(coins)\n    // Initialize dp table\n    dp := make([]int, amt+1)\n    dp[0] = 1\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in forward order\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a-coins[i-1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
    coin_change_ii.swift
    /* Coin change II: Space-optimized dynamic programming */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: amt + 1)\n    dp[0] = 1\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
    coin_change_ii.js
    /* Coin change II: Space-optimized dynamic programming */\nfunction coinChangeIIDPComp(coins, amt) {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.ts
    /* Coin change II: Space-optimized dynamic programming */\nfunction coinChangeIIDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.dart
    /* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  // Initialize dp table\n  List<int> dp = List.filled(amt + 1, 0);\n  dp[0] = 1;\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[a] = dp[a];\n      } else {\n        // Sum of the two options: not selecting and selecting coin i\n        dp[a] = dp[a] + dp[a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[amt];\n}\n
    coin_change_ii.rs
    /* Coin change II: Space-optimized dynamic programming */\nfn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Initialize dp table\n    let mut dp = vec![0; amt + 1];\n    dp[0] = 1;\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Initialize dp table\n    int *dp = calloc(amt + 1, sizeof(int));\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[amt];\n    // Free memory\n    free(dp);\n    return res;\n}\n
    coin_change_ii.kt
    /* Coin change II: Space-optimized dynamic programming */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Initialize dp table\n    val dp = IntArray(amt + 1)\n    dp[0] = 1\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
    coin_change_ii.rb
    ### Coin change II: space-optimized DP ###\ndef coin_change_ii_dp_comp(coins, amt)\n  n = coins.length\n  # Initialize dp table\n  dp = Array.new(amt + 1, 0)\n  dp[0] = 1\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[a] = dp[a]\n      else\n        # Sum of the two options: not selecting and selecting coin i\n        dp[a] = dp[a] + dp[a - coins[i - 1]]\n      end\n    end\n  end\n  dp[amt]\nend\n
    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"Chapter 9.   Graph","text":"

    Abstract

    In the journey of life, we are like nodes, connected by countless invisible edges.

    Each encounter and parting leaves a unique mark on this vast network graph.

    ","path":["Chapter 9. Graph","Chapter 9.   Graph"],"tags":[]},{"location":"chapter_graph/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 9.1   Graph
    • 9.2   Basic Operations on Graphs
    • 9.3   Graph Traversal
    • 9.4   Summary
    ","path":["Chapter 9. Graph","Chapter 9.   Graph"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   Graph","text":"

    A graph is a nonlinear data structure consisting of vertices and edges. We can abstractly represent a graph \\(G\\) as a set of vertices \\(V\\) and a set of edges \\(E\\). The following example shows a graph containing 5 vertices and 7 edges.

    \\[ \\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} \\]

    If we view vertices as nodes and edges as references (pointers) connecting them, we can regard a graph as an extension of the linked list data structure. As shown in Figure 9-1, compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex.

    Figure 9-1   Relationships among linked lists, trees, and graphs

    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#911-common-types-and-terminology-of-graphs","level":2,"title":"9.1.1   Common Types and Terminology of Graphs","text":"

    Graphs can be divided into undirected graphs and directed graphs based on whether edges have direction, as shown in Figure 9-2.

    • In undirected graphs, edges represent a \"bidirectional\" connection between two vertices, such as friendships on WeChat or QQ.
    • In directed graphs, edges have directionality, meaning edges \\(A \\rightarrow B\\) and \\(A \\leftarrow B\\) are independent of each other, such as following and follower relationships on Weibo or TikTok.

    Figure 9-2   Directed and undirected graphs

    Graphs can be divided into connected graphs and disconnected graphs based on whether all vertices are connected, as shown in Figure 9-3.

    • For connected graphs, starting from any vertex, all other vertices can be reached.
    • For disconnected graphs, starting from a certain vertex, at least one vertex cannot be reached.

    Figure 9-3   Connected and disconnected graphs

    We can also add a \"weight\" variable to edges, resulting in weighted graphs as shown in Figure 9-4. For example, in mobile games like \"Honor of Kings\", the system calculates the \"intimacy\" between players based on how long they have played together, and such intimacy networks can be represented using weighted graphs.

    Figure 9-4   Weighted and unweighted graphs

    Graph data structures include the following commonly used terms.

    • Adjacency: When two vertices are connected by an edge, these two vertices are said to be \"adjacent\". In Figure 9-4, the adjacent vertices of vertex 1 are vertices 2, 3, and 5.
    • Path: The sequence of edges from vertex A to vertex B is called a \"path\" from A to B. In Figure 9-4, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4.
    • Degree: The number of edges a vertex has. For directed graphs, in-degree indicates how many edges point to the vertex, and out-degree indicates how many edges leave the vertex.
    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#912-representation-of-graphs","level":2,"title":"9.1.2   Representation of Graphs","text":"

    Common representations of graphs include \"adjacency matrices\" and \"adjacency lists\". The following uses undirected graphs as examples.

    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#1-adjacency-matrix","level":3,"title":"1.   Adjacency Matrix","text":"

    Given a graph with \\(n\\) vertices, an adjacency matrix uses an \\(n \\times n\\) matrix to represent the graph, where each row (column) represents a vertex, and matrix elements represent edges, using \\(1\\) or \\(0\\) to indicate whether an edge exists between two vertices.

    As shown in Figure 9-5, let the adjacency matrix be \\(M\\) and the vertex list be \\(V\\). Then matrix element \\(M[i, j] = 1\\) indicates that an edge exists between vertex \\(V[i]\\) and vertex \\(V[j]\\), whereas \\(M[i, j] = 0\\) indicates no edge between the two vertices.

    Figure 9-5   Adjacency matrix representation of a graph

    Adjacency matrices have the following properties.

    • In simple graphs, vertices cannot connect to themselves, so the elements on the main diagonal of the adjacency matrix are meaningless.
    • For undirected graphs, edges in both directions are equivalent, so the adjacency matrix is symmetric about the main diagonal.
    • Replacing the \\(1\\) and \\(0\\) entries in the adjacency matrix with weights allows it to represent weighted graphs.

    When using adjacency matrices to represent graphs, we can directly access matrix elements to obtain edges, resulting in highly efficient addition, deletion, lookup, and modification operations, all with a time complexity of \\(O(1)\\). However, the space complexity of the matrix is \\(O(n^2)\\), which consumes significant memory.

    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#2-adjacency-list","level":3,"title":"2.   Adjacency List","text":"

    An adjacency list uses \\(n\\) linked lists to represent a graph, with linked list nodes representing vertices. The \\(i\\)-th linked list corresponds to vertex \\(i\\) and stores all adjacent vertices of that vertex (vertices connected to that vertex). Figure 9-6 shows an example of a graph stored using an adjacency list.

    Figure 9-6   Adjacency list representation of a graph

    Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than \\(n^2\\), making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so it is less time-efficient than an adjacency matrix.

    As shown in Figure 9-6, the structure of adjacency lists is very similar to separate chaining in hash tables, so we can use similar methods to improve efficiency. For example, when a linked list becomes long, it can be converted into an AVL tree or red-black tree, improving the time complexity from \\(O(n)\\) to \\(O(\\log n)\\); it can also be converted into a hash table, reducing the time complexity to \\(O(1)\\).

    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#913-common-applications-of-graphs","level":2,"title":"9.1.3   Common Applications of Graphs","text":"

    As shown in Table 9-1, many real-world systems can be modeled using graphs, and corresponding problems can be reduced to graph computation problems.

    Table 9-1   Common graphs in real life

    Vertices Edges Graph Computation Problem Social network Users Friend relationships Potential friend recommendation Subway lines Stations Connectivity between stations Shortest route recommendation Solar system Celestial bodies Gravitational forces between celestial bodies Planetary orbit calculation","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   Basic Operations on Graphs","text":"

    Basic operations on graphs can be divided into operations on \"edges\" and operations on \"vertices\". Their implementations differ depending on whether the graph is represented as an \"adjacency matrix\" or an \"adjacency list\".

    ","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#921-implementation-based-on-adjacency-matrix","level":2,"title":"9.2.1   Implementation Based on Adjacency Matrix","text":"

    Given an undirected graph with \\(n\\) vertices, the various operations are implemented as shown in Figure 9-7.

    • Adding or removing an edge: Directly modify the specified edge in the adjacency matrix, using \\(O(1)\\) time. Since it is an undirected graph, both directions of the edge need to be updated simultaneously.
    • Adding a vertex: Add a row and a column at the end of the adjacency matrix and fill them all with \\(0\\)s, using \\(O(n)\\) time.
    • Removing a vertex: Delete a row and a column in the adjacency matrix. The worst case occurs when removing the first row and column, requiring \\((n-1)^2\\) elements to be \"moved up and to the left\", thus using \\(O(n^2)\\) time.
    • Initialization: Given \\(n\\) vertices, initialize a vertex list vertices of length \\(n\\), using \\(O(n)\\) time; initialize an adjacency matrix adjMat of size \\(n \\times n\\), using \\(O(n^2)\\) time.
    <1><2><3><4><5>

    Figure 9-7   Initialization, adding and removing edges, adding and removing vertices in adjacency matrix

    The following is the implementation code for graphs represented using an adjacency matrix:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py
    class GraphAdjMat:\n    \"\"\"Undirected graph class based on adjacency matrix\"\"\"\n\n    def __init__(self, vertices: list[int], edges: list[list[int]]):\n        \"\"\"Constructor\"\"\"\n        # Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n        self.vertices: list[int] = []\n        # Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n        self.adj_mat: list[list[int]] = []\n        # Add vertices\n        for val in vertices:\n            self.add_vertex(val)\n        # Add edges\n        # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for e in edges:\n            self.add_edge(e[0], e[1])\n\n    def size(self) -> int:\n        \"\"\"Get the number of vertices\"\"\"\n        return len(self.vertices)\n\n    def add_vertex(self, val: int):\n        \"\"\"Add vertex\"\"\"\n        n = self.size()\n        # Add the value of the new vertex to the vertex list\n        self.vertices.append(val)\n        # Add a row to the adjacency matrix\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # Add a column to the adjacency matrix\n        for row in self.adj_mat:\n            row.append(0)\n\n    def remove_vertex(self, index: int):\n        \"\"\"Remove vertex\"\"\"\n        if index >= self.size():\n            raise IndexError()\n        # Remove the vertex at index from the vertex list\n        self.vertices.pop(index)\n        # Remove the row at index from the adjacency matrix\n        self.adj_mat.pop(index)\n        # Remove the column at index from the adjacency matrix\n        for row in self.adj_mat:\n            row.pop(index)\n\n    def add_edge(self, i: int, j: int):\n        \"\"\"Add edge\"\"\"\n        # Parameters i, j correspond to the vertices element indices\n        # Handle index out of bounds and equality\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (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        \"\"\"Remove edge\"\"\"\n        # Parameters i, j correspond to the vertices element indices\n        # Handle index out of bounds and equality\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        \"\"\"Print adjacency matrix\"\"\"\n        print(\"Vertex list =\", self.vertices)\n        print(\"Adjacency matrix =\")\n        print_matrix(self.adj_mat)\n
    graph_adjacency_matrix.cpp
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vector<int> vertices;       // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    vector<vector<int>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n  public:\n    /* Constructor */\n    GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n        // Add vertex\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const vector<int> &edge : edges) {\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int size() const {\n        return vertices.size();\n    }\n\n    /* Add vertex */\n    void addVertex(int val) {\n        int n = size();\n        // Add the value of the new vertex to the vertex list\n        vertices.push_back(val);\n        // Add a row to the adjacency matrix\n        adjMat.emplace_back(vector<int>(n, 0));\n        // Add a column to the adjacency matrix\n        for (vector<int> &row : adjMat) {\n            row.push_back(0);\n        }\n    }\n\n    /* Remove vertex */\n    void removeVertex(int index) {\n        if (index >= size()) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        // Remove the vertex at index from the vertex list\n        vertices.erase(vertices.begin() + index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.erase(adjMat.begin() + index);\n        // Remove the column at index from the adjacency matrix\n        for (vector<int> &row : adjMat) {\n            row.erase(row.begin() + index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    void addEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    void removeEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    void print() {\n        cout << \"Vertex list = \";\n        printVector(vertices);\n        cout << \"Adjacency matrix =\" << endl;\n        printVectorMatrix(adjMat);\n    }\n};\n
    graph_adjacency_matrix.java
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    List<Integer> vertices; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    List<List<Integer>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = new ArrayList<>();\n        this.adjMat = new ArrayList<>();\n        // Add vertex\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (int[] e : edges) {\n            addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    public int size() {\n        return vertices.size();\n    }\n\n    /* Add vertex */\n    public void addVertex(int val) {\n        int n = size();\n        // Add the value of the new vertex to the vertex list\n        vertices.add(val);\n        // Add a row to the adjacency matrix\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        // Add a column to the adjacency matrix\n        for (List<Integer> row : adjMat) {\n            row.add(0);\n        }\n    }\n\n    /* Remove vertex */\n    public void removeVertex(int index) {\n        if (index >= size())\n            throw new IndexOutOfBoundsException();\n        // Remove the vertex at index from the vertex list\n        vertices.remove(index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.remove(index);\n        // Remove the column at index from the adjacency matrix\n        for (List<Integer> row : adjMat) {\n            row.remove(index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void addEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat.get(i).set(j, 1);\n        adjMat.get(j).set(i, 1);\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void removeEdge(int i, int j) {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    public void print() {\n        System.out.print(\"Vertex list = \");\n        System.out.println(vertices);\n        System.out.println(\"Adjacency matrix =\");\n        PrintUtil.printMatrix(adjMat);\n    }\n}\n
    graph_adjacency_matrix.cs
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    List<int> vertices;     // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    List<List<int>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        foreach (int val in vertices) {\n            AddVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        foreach (int[] e in edges) {\n            AddEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int Size() {\n        return vertices.Count;\n    }\n\n    /* Add vertex */\n    public void AddVertex(int val) {\n        int n = Size();\n        // Add the value of the new vertex to the vertex list\n        vertices.Add(val);\n        // Add a row to the adjacency matrix\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // Add a column to the adjacency matrix\n        foreach (List<int> row in adjMat) {\n            row.Add(0);\n        }\n    }\n\n    /* Remove vertex */\n    public void RemoveVertex(int index) {\n        if (index >= Size())\n            throw new IndexOutOfRangeException();\n        // Remove the vertex at index from the vertex list\n        vertices.RemoveAt(index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.RemoveAt(index);\n        // Remove the column at index from the adjacency matrix\n        foreach (List<int> row in adjMat) {\n            row.RemoveAt(index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void AddEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void RemoveEdge(int i, int j) {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    public void Print() {\n        Console.Write(\"Vertex list = \");\n        PrintUtil.PrintList(vertices);\n        Console.WriteLine(\"Adjacency matrix =\");\n        PrintUtil.PrintMatrix(adjMat);\n    }\n}\n
    graph_adjacency_matrix.go
    /* Undirected graph class based on adjacency matrix */\ntype graphAdjMat struct {\n    // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    vertices []int\n    // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    adjMat [][]int\n}\n\n/* Constructor */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n    // Add vertex\n    n := len(vertices)\n    adjMat := make([][]int, n)\n    for i := range adjMat {\n        adjMat[i] = make([]int, n)\n    }\n    // Initialize graph\n    g := &graphAdjMat{\n        vertices: vertices,\n        adjMat:   adjMat,\n    }\n    // Add edge\n    // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    for i := range edges {\n        g.addEdge(edges[i][0], edges[i][1])\n    }\n    return g\n}\n\n/* Get the number of vertices */\nfunc (g *graphAdjMat) size() int {\n    return len(g.vertices)\n}\n\n/* Add vertex */\nfunc (g *graphAdjMat) addVertex(val int) {\n    n := g.size()\n    // Add the value of the new vertex to the vertex list\n    g.vertices = append(g.vertices, val)\n    // Add a row to the adjacency matrix\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // Add a column to the adjacency matrix\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i], 0)\n    }\n}\n\n/* Remove vertex */\nfunc (g *graphAdjMat) removeVertex(index int) {\n    if index >= g.size() {\n        return\n    }\n    // Remove the vertex at index from the vertex list\n    g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n    // Remove the row at index from the adjacency matrix\n    g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n    // Remove the column at index from the adjacency matrix\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n    }\n}\n\n/* Add edge */\n// Parameters i, j correspond to the vertices element indices\nfunc (g *graphAdjMat) addEdge(i, j int) {\n    // Handle index out of bounds and equality\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    // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    g.adjMat[i][j] = 1\n    g.adjMat[j][i] = 1\n}\n\n/* Remove edge */\n// Parameters i, j correspond to the vertices element indices\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n    // Handle index out of bounds and equality\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/* Print adjacency matrix */\nfunc (g *graphAdjMat) print() {\n    fmt.Printf(\"\\tVertex list = %v\\n\", g.vertices)\n    fmt.Printf(\"\\tAdjacency matrix = \\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
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    private var vertices: [Int] // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    private var adjMat: [[Int]] // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    init(vertices: [Int], edges: [[Int]]) {\n        self.vertices = []\n        adjMat = []\n        // Add vertex\n        for val in vertices {\n            addVertex(val: val)\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for e in edges {\n            addEdge(i: e[0], j: e[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    func size() -> Int {\n        vertices.count\n    }\n\n    /* Add vertex */\n    func addVertex(val: Int) {\n        let n = size()\n        // Add the value of the new vertex to the vertex list\n        vertices.append(val)\n        // Add a row to the adjacency matrix\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // Add a column to the adjacency matrix\n        for i in adjMat.indices {\n            adjMat[i].append(0)\n        }\n    }\n\n    /* Remove vertex */\n    func removeVertex(index: Int) {\n        if index >= size() {\n            fatalError(\"Out of bounds\")\n        }\n        // Remove the vertex at index from the vertex list\n        vertices.remove(at: index)\n        // Remove the row at index from the adjacency matrix\n        adjMat.remove(at: index)\n        // Remove the column at index from the adjacency matrix\n        for i in adjMat.indices {\n            adjMat[i].remove(at: index)\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    func addEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Out of bounds\")\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    func removeEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Out of bounds\")\n        }\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Print adjacency matrix */\n    func print() {\n        Swift.print(\"Vertex list = \", terminator: \"\")\n        Swift.print(vertices)\n        Swift.print(\"Adjacency matrix =\")\n        PrintUtil.printMatrix(matrix: adjMat)\n    }\n}\n
    graph_adjacency_matrix.js
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vertices; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    constructor(vertices, edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size() {\n        return this.vertices.length;\n    }\n\n    /* Add vertex */\n    addVertex(val) {\n        const n = this.size();\n        // Add the value of the new vertex to the vertex list\n        this.vertices.push(val);\n        // Add a row to the adjacency matrix\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Add a column to the adjacency matrix\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    removeVertex(index) {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Remove the vertex at index from the vertex list\n        this.vertices.splice(index, 1);\n\n        // Remove the row at index from the adjacency matrix\n        this.adjMat.splice(index, 1);\n        // Remove the column at index from the adjacency matrix\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    addEdge(i, j) {\n        // Handle index out of bounds and equality\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        // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    removeEdge(i, j) {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    print() {\n        console.log('Vertex list = ', this.vertices);\n        console.log('Adjacency matrix =', this.adjMat);\n    }\n}\n
    graph_adjacency_matrix.ts
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vertices: number[]; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    adjMat: number[][]; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    constructor(vertices: number[], edges: number[][]) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size(): number {\n        return this.vertices.length;\n    }\n\n    /* Add vertex */\n    addVertex(val: number): void {\n        const n: number = this.size();\n        // Add the value of the new vertex to the vertex list\n        this.vertices.push(val);\n        // Add a row to the adjacency matrix\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Add a column to the adjacency matrix\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    removeVertex(index: number): void {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Remove the vertex at index from the vertex list\n        this.vertices.splice(index, 1);\n\n        // Remove the row at index from the adjacency matrix\n        this.adjMat.splice(index, 1);\n        // Remove the column at index from the adjacency matrix\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    addEdge(i: number, j: number): void {\n        // Handle index out of bounds and equality\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        // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    removeEdge(i: number, j: number): void {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    print(): void {\n        console.log('Vertex list = ', this.vertices);\n        console.log('Adjacency matrix =', this.adjMat);\n    }\n}\n
    graph_adjacency_matrix.dart
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n  List<int> vertices = []; // Vertex elements, elements represent \"vertex values\", indices represent \"vertex indices\"\n  List<List<int>> adjMat = []; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n  /* Constructor */\n  GraphAdjMat(List<int> vertices, List<List<int>> edges) {\n    this.vertices = [];\n    this.adjMat = [];\n    // Add vertex\n    for (int val in vertices) {\n      addVertex(val);\n    }\n    // Add edge\n    // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    for (List<int> e in edges) {\n      addEdge(e[0], e[1]);\n    }\n  }\n\n  /* Get the number of vertices */\n  int size() {\n    return vertices.length;\n  }\n\n  /* Add vertex */\n  void addVertex(int val) {\n    int n = size();\n    // Add the value of the new vertex to the vertex list\n    vertices.add(val);\n    // Add a row to the adjacency matrix\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // Add a column to the adjacency matrix\n    for (List<int> row in adjMat) {\n      row.add(0);\n    }\n  }\n\n  /* Remove vertex */\n  void removeVertex(int index) {\n    if (index >= size()) {\n      throw IndexError;\n    }\n    // Remove the vertex at index from the vertex list\n    vertices.removeAt(index);\n    // Remove the row at index from the adjacency matrix\n    adjMat.removeAt(index);\n    // Remove the column at index from the adjacency matrix\n    for (List<int> row in adjMat) {\n      row.removeAt(index);\n    }\n  }\n\n  /* Add edge */\n  // Parameters i, j correspond to the vertices element indices\n  void addEdge(int i, int j) {\n    // Handle index out of bounds and equality\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    adjMat[i][j] = 1;\n    adjMat[j][i] = 1;\n  }\n\n  /* Remove edge */\n  // Parameters i, j correspond to the vertices element indices\n  void removeEdge(int i, int j) {\n    // Handle index out of bounds and equality\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  /* Print adjacency matrix */\n  void printAdjMat() {\n    print(\"Vertex list = $vertices\");\n    print(\"Adjacency matrix = \");\n    printMatrix(adjMat);\n  }\n}\n
    graph_adjacency_matrix.rs
    /* Undirected graph type based on adjacency matrix */\npub struct GraphAdjMat {\n    // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    pub vertices: Vec<i32>,\n    // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    pub adj_mat: Vec<Vec<i32>>,\n}\n\nimpl GraphAdjMat {\n    /* Constructor */\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        // Add vertex\n        for val in vertices {\n            graph.add_vertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for edge in edges {\n            graph.add_edge(edge[0], edge[1])\n        }\n\n        graph\n    }\n\n    /* Get the number of vertices */\n    pub fn size(&self) -> usize {\n        self.vertices.len()\n    }\n\n    /* Add vertex */\n    pub fn add_vertex(&mut self, val: i32) {\n        let n = self.size();\n        // Add the value of the new vertex to the vertex list\n        self.vertices.push(val);\n        // Add a row to the adjacency matrix\n        self.adj_mat.push(vec![0; n]);\n        // Add a column to the adjacency matrix\n        for row in self.adj_mat.iter_mut() {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    pub fn remove_vertex(&mut self, index: usize) {\n        if index >= self.size() {\n            panic!(\"index error\")\n        }\n        // Remove the vertex at index from the vertex list\n        self.vertices.remove(index);\n        // Remove the row at index from the adjacency matrix\n        self.adj_mat.remove(index);\n        // Remove the column at index from the adjacency matrix\n        for row in self.adj_mat.iter_mut() {\n            row.remove(index);\n        }\n    }\n\n    /* Add edge */\n    pub fn add_edge(&mut self, i: usize, j: usize) {\n        // Parameters i, j correspond to the vertices element indices\n        // Handle index out of bounds and equality\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        self.adj_mat[i][j] = 1;\n        self.adj_mat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    pub fn remove_edge(&mut self, i: usize, j: usize) {\n        // Parameters i, j correspond to the vertices element indices\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    pub fn print(&self) {\n        println!(\"Vertex list = {:?}\", self.vertices);\n        println!(\"Adjacency matrix =\");\n        println!(\"[\");\n        for row in &self.adj_mat {\n            println!(\"  {:?},\", row);\n        }\n        println!(\"]\")\n    }\n}\n
    graph_adjacency_matrix.c
    /* Undirected graph structure based on adjacency matrix */\ntypedef struct {\n    int vertices[MAX_SIZE];\n    int adjMat[MAX_SIZE][MAX_SIZE];\n    int size;\n} GraphAdjMat;\n\n/* Constructor */\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/* Destructor */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n    free(graph);\n}\n\n/* Add vertex */\nvoid addVertex(GraphAdjMat *graph, int val) {\n    if (graph->size == MAX_SIZE) {\n        fprintf(stderr, \"Graph vertex count has reached maximum\\n\");\n        return;\n    }\n    // Add nth vertex and zero nth row and column\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/* Remove vertex */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n    if (index < 0 || index >= graph->size) {\n        fprintf(stderr, \"Vertex index out of bounds\\n\");\n        return;\n    }\n    // Remove the vertex at index from the vertex list\n    for (int i = index; i < graph->size - 1; i++) {\n        graph->vertices[i] = graph->vertices[i + 1];\n    }\n    // Remove the row at index from the adjacency matrix\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    // Remove the column at index from the adjacency matrix\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/* Add edge */\n// Parameters i, j correspond to the vertices element indices\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, \"Edge index out of bounds or equal\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 1;\n    graph->adjMat[j][i] = 1;\n}\n\n/* Remove edge */\n// Parameters i, j correspond to the vertices element indices\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, \"Edge index out of bounds or equal\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 0;\n    graph->adjMat[j][i] = 0;\n}\n\n/* Print adjacency matrix */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n    printf(\"Vertex list = \");\n    printArray(graph->vertices, graph->size);\n    printf(\"Adjacency matrix =\\n\");\n    for (int i = 0; i < graph->size; i++) {\n        printArray(graph->adjMat[i], graph->size);\n    }\n}\n
    graph_adjacency_matrix.kt
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat(vertices: IntArray, edges: Array<IntArray>) {\n    val vertices = mutableListOf<Int>() // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    val adjMat = mutableListOf<MutableList<Int>>() // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    init {\n        // Add vertex\n        for (vertex in vertices) {\n            addVertex(vertex)\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (edge in edges) {\n            addEdge(edge[0], edge[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    fun size(): Int {\n        return vertices.size\n    }\n\n    /* Add vertex */\n    fun addVertex(_val: Int) {\n        val n = size()\n        // Add the value of the new vertex to the vertex list\n        vertices.add(_val)\n        // Add a row to the adjacency matrix\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // Add a column to the adjacency matrix\n        for (row in adjMat) {\n            row.add(0)\n        }\n    }\n\n    /* Remove vertex */\n    fun removeVertex(index: Int) {\n        if (index >= size())\n            throw IndexOutOfBoundsException()\n        // Remove the vertex at index from the vertex list\n        vertices.removeAt(index)\n        // Remove the row at index from the adjacency matrix\n        adjMat.removeAt(index)\n        // Remove the column at index from the adjacency matrix\n        for (row in adjMat) {\n            row.removeAt(index)\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    fun addEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    fun removeEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    fun print() {\n        print(\"Vertex list = \")\n        println(vertices)\n        println(\"Adjacency matrix =\")\n        printMatrix(adjMat)\n    }\n}\n
    graph_adjacency_matrix.rb
    ### Undirected graph class based on adjacency matrix ###\nclass GraphAdjMat\n  def initialize(vertices, edges)\n    ### Constructor ###\n    # Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    @vertices = []\n    # Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    @adj_mat = []\n    # Add vertex\n    vertices.each { |val| add_vertex(val) }\n    # Add edge\n    # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    edges.each { |e| add_edge(e[0], e[1]) }\n  end\n\n  ### Get number of vertices ###\n  def size\n    @vertices.length\n  end\n\n  ### Add vertex ###\n  def add_vertex(val)\n    n = size\n    # Add the value of the new vertex to the vertex list\n    @vertices << val\n    # Add a row to the adjacency matrix\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # Add a column to the adjacency matrix\n    @adj_mat.each { |row| row << 0 }\n  end\n\n  ### Delete vertex ###\n  def remove_vertex(index)\n    raise IndexError if index >= size\n\n    # Remove the vertex at index from the vertex list\n    @vertices.delete_at(index)\n    # Remove the row at index from the adjacency matrix\n    @adj_mat.delete_at(index)\n    # Remove the column at index from the adjacency matrix\n    @adj_mat.each { |row| row.delete_at(index) }\n  end\n\n  ### Add edge ###\n  def add_edge(i, j)\n    # Parameters i, j correspond to the vertices element indices\n    # Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    @adj_mat[i][j] = 1\n    @adj_mat[j][i] = 1\n  end\n\n  ### Delete edge ###\n  def remove_edge(i, j)\n    # Parameters i, j correspond to the vertices element indices\n    # Handle index out of bounds and equality\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  ### Print adjacency matrix ###\n  def __print__\n    puts \"Vertex list = #{@vertices}\"\n    puts 'Adjacency matrix ='\n    print_matrix(@adj_mat)\n  end\nend\n
    ","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#922-implementation-based-on-adjacency-list","level":2,"title":"9.2.2   Implementation Based on Adjacency List","text":"

    Given an undirected graph with a total of \\(n\\) vertices and \\(m\\) edges, the various operations can be implemented as shown in Figure 9-8.

    • Adding an edge: Add the edge at the end of the corresponding vertex's linked list, using \\(O(1)\\) time. Since it is an undirected graph, edges in both directions need to be added simultaneously.
    • Removing an edge: Find and remove the specified edge in the corresponding vertex's linked list, using \\(O(m)\\) time. In an undirected graph, edges in both directions need to be removed simultaneously.
    • Adding a vertex: Add a linked list to the adjacency list, with the new vertex as the head node, using \\(O(1)\\) time.
    • Removing a vertex: Traverse the entire adjacency list and remove all edges containing the specified vertex, using \\(O(n + m)\\) time.
    • Initialization: Create \\(n\\) vertices and \\(2m\\) edges in the adjacency list, using \\(O(n + m)\\) time.
    <1><2><3><4><5>

    Figure 9-8   Initialization, adding and removing edges, adding and removing vertices in adjacency list

    The following code shows the adjacency list implementation. Compared with Figure 9-8, the actual code differs in the following ways.

    • For convenience in adding and removing vertices, and to simplify the code, we use lists (dynamic arrays) instead of linked lists.
    • A hash table is used to store the adjacency list, where key is the vertex instance and value is the list (linked list) of adjacent vertices for that vertex.

    Additionally, we use the Vertex class to represent vertices in the adjacency list for the following reason: if we used list indices to distinguish different vertices, as with adjacency matrices, then to delete the vertex at index \\(i\\), we would need to traverse the entire adjacency list and decrement all indices greater than \\(i\\) by \\(1\\), which is very inefficient. However, if each vertex is a unique Vertex instance, deleting one vertex does not require modifying the others.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py
    class GraphAdjList:\n    \"\"\"Undirected graph class based on adjacency list\"\"\"\n\n    def __init__(self, edges: list[list[Vertex]]):\n        \"\"\"Constructor\"\"\"\n        # Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n        self.adj_list = dict[Vertex, list[Vertex]]()\n        # Add all vertices and edges\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        \"\"\"Get the number of vertices\"\"\"\n        return len(self.adj_list)\n\n    def add_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Add edge\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Add edge 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        \"\"\"Remove edge\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Remove edge 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        \"\"\"Add vertex\"\"\"\n        if vet in self.adj_list:\n            return\n        # Add a new linked list in the adjacency list\n        self.adj_list[vet] = []\n\n    def remove_vertex(self, vet: Vertex):\n        \"\"\"Remove vertex\"\"\"\n        if vet not in self.adj_list:\n            raise ValueError()\n        # Remove the linked list corresponding to vertex vet in the adjacency list\n        self.adj_list.pop(vet)\n        # Traverse the linked lists of other vertices and remove all edges containing 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        \"\"\"Print adjacency list\"\"\"\n        print(\"Adjacency list =\")\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n  public:\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n    /* Remove specified node from 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    /* Constructor */\n    GraphAdjList(const vector<vector<Vertex *>> &edges) {\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    int size() {\n        return adjList.size();\n    }\n\n    /* Add edge */\n    void addEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"Vertex does not exist\");\n        // Add edge vet1 - vet2\n        adjList[vet1].push_back(vet2);\n        adjList[vet2].push_back(vet1);\n    }\n\n    /* Remove edge */\n    void removeEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"Vertex does not exist\");\n        // Remove edge vet1 - vet2\n        remove(adjList[vet1], vet2);\n        remove(adjList[vet2], vet1);\n    }\n\n    /* Add vertex */\n    void addVertex(Vertex *vet) {\n        if (adjList.count(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList[vet] = vector<Vertex *>();\n    }\n\n    /* Remove vertex */\n    void removeVertex(Vertex *vet) {\n        if (!adjList.count(vet))\n            throw invalid_argument(\"Vertex does not exist\");\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.erase(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (auto &adj : adjList) {\n            remove(adj.second, vet);\n        }\n    }\n\n    /* Print adjacency list */\n    void print() {\n        cout << \"Adjacency list =\" << 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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    Map<Vertex, List<Vertex>> adjList;\n\n    /* Constructor */\n    public GraphAdjList(Vertex[][] edges) {\n        this.adjList = new HashMap<>();\n        // Add all vertices and edges\n        for (Vertex[] edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    public int size() {\n        return adjList.size();\n    }\n\n    /* Add edge */\n    public void addEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Add edge vet1 - vet2\n        adjList.get(vet1).add(vet2);\n        adjList.get(vet2).add(vet1);\n    }\n\n    /* Remove edge */\n    public void removeEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Remove edge vet1 - vet2\n        adjList.get(vet1).remove(vet2);\n        adjList.get(vet2).remove(vet1);\n    }\n\n    /* Add vertex */\n    public void addVertex(Vertex vet) {\n        if (adjList.containsKey(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList.put(vet, new ArrayList<>());\n    }\n\n    /* Remove vertex */\n    public void removeVertex(Vertex vet) {\n        if (!adjList.containsKey(vet))\n            throw new IllegalArgumentException();\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.remove(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (List<Vertex> list : adjList.values()) {\n            list.remove(vet);\n        }\n    }\n\n    /* Print adjacency list */\n    public void print() {\n        System.out.println(\"Adjacency list =\");\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    public Dictionary<Vertex, List<Vertex>> adjList;\n\n    /* Constructor */\n    public GraphAdjList(Vertex[][] edges) {\n        adjList = [];\n        // Add all vertices and edges\n        foreach (Vertex[] edge in edges) {\n            AddVertex(edge[0]);\n            AddVertex(edge[1]);\n            AddEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int Size() {\n        return adjList.Count;\n    }\n\n    /* Add edge */\n    public void AddEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Add edge vet1 - vet2\n        adjList[vet1].Add(vet2);\n        adjList[vet2].Add(vet1);\n    }\n\n    /* Remove edge */\n    public void RemoveEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Remove edge vet1 - vet2\n        adjList[vet1].Remove(vet2);\n        adjList[vet2].Remove(vet1);\n    }\n\n    /* Add vertex */\n    public void AddVertex(Vertex vet) {\n        if (adjList.ContainsKey(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList.Add(vet, []);\n    }\n\n    /* Remove vertex */\n    public void RemoveVertex(Vertex vet) {\n        if (!adjList.ContainsKey(vet))\n            throw new InvalidOperationException();\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.Remove(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        foreach (List<Vertex> list in adjList.Values) {\n            list.Remove(vet);\n        }\n    }\n\n    /* Print adjacency list */\n    public void Print() {\n        Console.WriteLine(\"Adjacency list =\");\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
    /* Undirected graph class based on adjacency list */\ntype graphAdjList struct {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList map[Vertex][]Vertex\n}\n\n/* Constructor */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n    g := &graphAdjList{\n        adjList: make(map[Vertex][]Vertex),\n    }\n    // Add all vertices and edges\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/* Get the number of vertices */\nfunc (g *graphAdjList) size() int {\n    return len(g.adjList)\n}\n\n/* Add edge */\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    // Add edge vet1 - vet2, add anonymous struct{},\n    g.adjList[vet1] = append(g.adjList[vet1], vet2)\n    g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* Remove edge */\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    // Remove edge vet1 - vet2\n    g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n    g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* Add vertex */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if ok {\n        return\n    }\n    // Add a new linked list in the adjacency list\n    g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* Remove vertex */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if !ok {\n        panic(\"error\")\n    }\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    delete(g.adjList, vet)\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    for v, list := range g.adjList {\n        g.adjList[v] = DeleteSliceElms(list, vet)\n    }\n}\n\n/* Print adjacency list */\nfunc (g *graphAdjList) print() {\n    var builder strings.Builder\n    fmt.Printf(\"Adjacency list = \\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    public private(set) var adjList: [Vertex: [Vertex]]\n\n    /* Constructor */\n    public init(edges: [[Vertex]]) {\n        adjList = [:]\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    public func size() -> Int {\n        adjList.count\n    }\n\n    /* Add edge */\n    public func addEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Invalid parameter\")\n        }\n        // Add edge vet1 - vet2\n        adjList[vet1]?.append(vet2)\n        adjList[vet2]?.append(vet1)\n    }\n\n    /* Remove edge */\n    public func removeEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Invalid parameter\")\n        }\n        // Remove edge vet1 - vet2\n        adjList[vet1]?.removeAll { $0 == vet2 }\n        adjList[vet2]?.removeAll { $0 == vet1 }\n    }\n\n    /* Add vertex */\n    public func addVertex(vet: Vertex) {\n        if adjList[vet] != nil {\n            return\n        }\n        // Add a new linked list in the adjacency list\n        adjList[vet] = []\n    }\n\n    /* Remove vertex */\n    public func removeVertex(vet: Vertex) {\n        if adjList[vet] == nil {\n            fatalError(\"Invalid parameter\")\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.removeValue(forKey: vet)\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for key in adjList.keys {\n            adjList[key]?.removeAll { $0 == vet }\n        }\n    }\n\n    /* Print adjacency list */\n    public func print() {\n        Swift.print(\"Adjacency list =\")\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList;\n\n    /* Constructor */\n    constructor(edges) {\n        this.adjList = new Map();\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    size() {\n        return this.adjList.size;\n    }\n\n    /* Add edge */\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        // Add edge vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Remove edge */\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        // Remove edge 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    /* Add vertex */\n    addVertex(vet) {\n        if (this.adjList.has(vet)) return;\n        // Add a new linked list in the adjacency list\n        this.adjList.set(vet, []);\n    }\n\n    /* Remove vertex */\n    removeVertex(vet) {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        this.adjList.delete(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing 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    /* Print adjacency list */\n    print() {\n        console.log('Adjacency list =');\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList: Map<Vertex, Vertex[]>;\n\n    /* Constructor */\n    constructor(edges: Vertex[][]) {\n        this.adjList = new Map();\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    size(): number {\n        return this.adjList.size;\n    }\n\n    /* Add edge */\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        // Add edge vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Remove edge */\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        // Remove edge 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    /* Add vertex */\n    addVertex(vet: Vertex): void {\n        if (this.adjList.has(vet)) return;\n        // Add a new linked list in the adjacency list\n        this.adjList.set(vet, []);\n    }\n\n    /* Remove vertex */\n    removeVertex(vet: Vertex): void {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        this.adjList.delete(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing 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    /* Print adjacency list */\n    print(): void {\n        console.log('Adjacency list =');\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n  // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n  Map<Vertex, List<Vertex>> adjList = {};\n\n  /* Constructor */\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  /* Get the number of vertices */\n  int size() {\n    return adjList.length;\n  }\n\n  /* Add edge */\n  void addEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Add edge vet1 - vet2\n    adjList[vet1]!.add(vet2);\n    adjList[vet2]!.add(vet1);\n  }\n\n  /* Remove edge */\n  void removeEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Remove edge vet1 - vet2\n    adjList[vet1]!.remove(vet2);\n    adjList[vet2]!.remove(vet1);\n  }\n\n  /* Add vertex */\n  void addVertex(Vertex vet) {\n    if (adjList.containsKey(vet)) return;\n    // Add a new linked list in the adjacency list\n    adjList[vet] = [];\n  }\n\n  /* Remove vertex */\n  void removeVertex(Vertex vet) {\n    if (!adjList.containsKey(vet)) {\n      throw ArgumentError;\n    }\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    adjList.remove(vet);\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    adjList.forEach((key, value) {\n      value.remove(vet);\n    });\n  }\n\n  /* Print adjacency list */\n  void printAdjList() {\n    print(\"Adjacency list =\");\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
    /* Undirected graph type based on adjacency list */\npub struct GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    pub adj_list: HashMap<Vertex, Vec<Vertex>>, // maybe HashSet<Vertex> for value part is better?\n}\n\nimpl GraphAdjList {\n    /* Constructor */\n    pub fn new(edges: Vec<[Vertex; 2]>) -> Self {\n        let mut graph = GraphAdjList {\n            adj_list: HashMap::new(),\n        };\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    #[allow(unused)]\n    pub fn size(&self) -> usize {\n        self.adj_list.len()\n    }\n\n    /* Add edge */\n    pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Add edge 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    /* Remove edge */\n    #[allow(unused)]\n    pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Remove edge 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    /* Add vertex */\n    pub fn add_vertex(&mut self, vet: Vertex) {\n        if self.adj_list.contains_key(&vet) {\n            return;\n        }\n        // Add a new linked list in the adjacency list\n        self.adj_list.insert(vet, vec![]);\n    }\n\n    /* Remove vertex */\n    #[allow(unused)]\n    pub fn remove_vertex(&mut self, vet: Vertex) {\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        self.adj_list.remove(&vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for list in self.adj_list.values_mut() {\n            list.retain(|&v| v != vet);\n        }\n    }\n\n    /* Print adjacency list */\n    pub fn print(&self) {\n        println!(\"Adjacency list =\");\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
    /* Node structure */\ntypedef struct AdjListNode {\n    Vertex *vertex;           // Vertex\n    struct AdjListNode *next; // Successor node\n} AdjListNode;\n\n/* Find node corresponding to vertex */\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/* Add edge helper function */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n    node->vertex = vet;\n    // Head insertion\n    node->next = head->next;\n    head->next = node;\n}\n\n/* Remove edge helper function */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *pre = head;\n    AdjListNode *cur = head->next;\n    // Search for node corresponding to vet in list\n    while (cur != NULL && cur->vertex != vet) {\n        pre = cur;\n        cur = cur->next;\n    }\n    if (cur == NULL)\n        return;\n    // Remove node corresponding to vet from list\n    pre->next = cur->next;\n    // Free memory\n    free(cur);\n}\n\n/* Undirected graph class based on adjacency list */\ntypedef struct {\n    AdjListNode *heads[MAX_SIZE]; // Node array\n    int size;                     // Node count\n} GraphAdjList;\n\n/* Constructor */\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/* Destructor */\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/* Find node corresponding to vertex */\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/* Add edge */\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    // Add edge vet1 - vet2\n    addEdgeHelper(head1, vet2);\n    addEdgeHelper(head2, vet1);\n}\n\n/* Remove edge */\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    // Remove edge vet1 - vet2\n    removeEdgeHelper(head1, head2->vertex);\n    removeEdgeHelper(head2, head1->vertex);\n}\n\n/* Add vertex */\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    // Add a new linked list in the adjacency list\n    graph->heads[graph->size++] = head;\n}\n\n/* Remove vertex */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n    AdjListNode *node = findNode(graph, vet);\n    assert(node != NULL);\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    AdjListNode *cur = node, *pre = NULL;\n    while (cur) {\n        pre = cur;\n        cur = cur->next;\n        free(pre);\n    }\n    // Traverse the linked lists of other vertices and remove all edges containing 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    // Move vertices after this vertex forward to fill gap\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList(edges: Array<Array<Vertex?>>) {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    val adjList = HashMap<Vertex, MutableList<Vertex>>()\n\n    /* Constructor */\n    init {\n        // Add all vertices and edges\n        for (edge in edges) {\n            addVertex(edge[0]!!)\n            addVertex(edge[1]!!)\n            addEdge(edge[0]!!, edge[1]!!)\n        }\n    }\n\n    /* Get the number of vertices */\n    fun size(): Int {\n        return adjList.size\n    }\n\n    /* Add edge */\n    fun addEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Add edge vet1 - vet2\n        adjList[vet1]?.add(vet2)\n        adjList[vet2]?.add(vet1)\n    }\n\n    /* Remove edge */\n    fun removeEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Remove edge vet1 - vet2\n        adjList[vet1]?.remove(vet2)\n        adjList[vet2]?.remove(vet1)\n    }\n\n    /* Add vertex */\n    fun addVertex(vet: Vertex) {\n        if (adjList.containsKey(vet))\n            return\n        // Add a new linked list in the adjacency list\n        adjList[vet] = mutableListOf()\n    }\n\n    /* Remove vertex */\n    fun removeVertex(vet: Vertex) {\n        if (!adjList.containsKey(vet))\n            throw IllegalArgumentException()\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.remove(vet)\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (list in adjList.values) {\n            list.remove(vet)\n        }\n    }\n\n    /* Print adjacency list */\n    fun print() {\n        println(\"Adjacency list =\")\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
    ### Undirected graph class based on adjacency list ###\nclass GraphAdjList\n  attr_reader :adj_list\n\n  ### Constructor ###\n  def initialize(edges)\n    # Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    @adj_list = {}\n    # Add all vertices and edges\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  ### Get number of vertices ###\n  def size\n    @adj_list.length\n  end\n\n  ### Add edge ###\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  ### Delete edge ###\n  def remove_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    # Remove edge vet1 - vet2\n    @adj_list[vet1].delete(vet2)\n    @adj_list[vet2].delete(vet1)\n  end\n\n  ### Add vertex ###\n  def add_vertex(vet)\n    return if @adj_list.include?(vet)\n\n    # Add a new linked list in the adjacency list\n    @adj_list[vet] = []\n  end\n\n  ### Delete vertex ###\n  def remove_vertex(vet)\n    raise ArgumentError unless @adj_list.include?(vet)\n\n    # Remove the linked list corresponding to vertex vet in the adjacency list\n    @adj_list.delete(vet)\n    # Traverse the linked lists of other vertices and remove all edges containing 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  ### Print adjacency list ###\n  def __print__\n    puts 'Adjacency list ='\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":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#923-efficiency-comparison","level":2,"title":"9.2.3   Efficiency Comparison","text":"

    Assuming the graph has \\(n\\) vertices and \\(m\\) edges, Table 9-2 compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation used in this section, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables.

    Table 9-2   Comparison of adjacency matrix and adjacency list

    Adjacency matrix Adjacency list (linked list) Adjacency list (hash table) Determine adjacency \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Add an edge \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Remove an edge \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Add a vertex \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) Remove a vertex \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) Memory space usage \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\)

    Observing Table 9-2, it appears that the adjacency list (hash table) has the best time efficiency and space efficiency. However, in practice, operating on edges in the adjacency matrix is more efficient, requiring only a single array access or assignment operation. Overall, adjacency matrices embody the principle of \"trading space for time\", while adjacency lists embody \"trading time for space\".

    ","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   Graph Traversal","text":"

    Trees represent \"one-to-many\" relationships, while graphs have a higher degree of freedom and can represent any \"many-to-many\" relationships. Therefore, we can view trees as a special case of graphs. Clearly, tree traversal operations are also a special case of graph traversal operations.

    Both graphs and trees require the application of search algorithms to implement traversal operations. Graph traversal methods can also be divided into two types: breadth-first traversal and depth-first traversal.

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931-breadth-first-search","level":2,"title":"9.3.1   Breadth-First Search","text":"

    Breadth-first search proceeds from near to far: starting from a given node, it always visits the nearest vertices first and expands outward layer by layer. As shown in Figure 9-9, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited.

    Figure 9-9   Breadth-first search of a graph

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1-algorithm-implementation","level":3,"title":"1.   Algorithm Implementation","text":"

    BFS is typically implemented with the help of a queue, as shown in the code below. The queue has a \"first in, first out\" property, which aligns with the BFS idea of \"near to far\".

    1. Add the starting vertex startVet to the queue and begin the loop.
    2. In each iteration of the loop, pop the vertex at the front of the queue and record it as visited, then add all adjacent vertices of that vertex to the back of the queue.
    3. Repeat step 2. until all vertices have been visited.

    To prevent revisiting vertices, we use a hash set visited to record which nodes have been visited.

    Tip

    A hash set can be viewed as a hash table that stores only key without storing value. It supports insertion, deletion, lookup, and update operations on key in \\(O(1)\\) time. Based on the uniqueness of key, hash sets are typically used for data deduplication and similar scenarios.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py
    def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Breadth-first traversal\"\"\"\n    # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n    # Vertex traversal sequence\n    res = []\n    # Hash set for recording vertices that have been visited\n    visited = set[Vertex]([start_vet])\n    # Queue used to implement BFS\n    que = deque[Vertex]([start_vet])\n    # Starting from vertex vet, loop until all vertices are visited\n    while len(que) > 0:\n        vet = que.popleft()  # Dequeue the front vertex\n        res.append(vet)  # Record visited vertex\n        # Traverse all adjacent vertices of this vertex\n        for adj_vet in graph.adj_list[vet]:\n            if adj_vet in visited:\n                continue  # Skip vertices that have been visited\n            que.append(adj_vet)  # Only enqueue unvisited vertices\n            visited.add(adj_vet)  # Mark this vertex as visited\n    # Return vertex traversal sequence\n    return res\n
    graph_bfs.cpp
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n    // Vertex traversal sequence\n    vector<Vertex *> res;\n    // Hash set for recording vertices that have been visited\n    unordered_set<Vertex *> visited = {startVet};\n    // Queue used to implement BFS\n    queue<Vertex *> que;\n    que.push(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.empty()) {\n        Vertex *vet = que.front();\n        que.pop();          // Dequeue the front vertex\n        res.push_back(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (auto adjVet : graph.adjList[vet]) {\n            if (visited.count(adjVet))\n                continue;            // Skip vertices that have been visited\n            que.push(adjVet);        // Only enqueue unvisited vertices\n            visited.emplace(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.java
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = new ArrayList<>();\n    // Hash set for recording vertices that have been visited\n    Set<Vertex> visited = new HashSet<>();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    Queue<Vertex> que = new LinkedList<>();\n    que.offer(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.isEmpty()) {\n        Vertex vet = que.poll(); // Dequeue the front vertex\n        res.add(vet);            // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (Vertex adjVet : graph.adjList.get(vet)) {\n            if (visited.contains(adjVet))\n                continue;        // Skip vertices that have been visited\n            que.offer(adjVet);   // Only enqueue unvisited vertices\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.cs
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = [];\n    // Hash set for recording vertices that have been visited\n    HashSet<Vertex> visited = [startVet];\n    // Queue used to implement BFS\n    Queue<Vertex> que = new();\n    que.Enqueue(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.Count > 0) {\n        Vertex vet = que.Dequeue(); // Dequeue the front vertex\n        res.Add(vet);               // Record visited vertex\n        foreach (Vertex adjVet in graph.adjList[vet]) {\n            if (visited.Contains(adjVet)) {\n                continue;          // Skip vertices that have been visited\n            }\n            que.Enqueue(adjVet);   // Only enqueue unvisited vertices\n            visited.Add(adjVet);   // Mark this vertex as visited\n        }\n    }\n\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.go
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Vertex traversal sequence\n    res := make([]Vertex, 0)\n    // Hash set for recording vertices that have been visited\n    visited := make(map[Vertex]struct{})\n    visited[startVet] = struct{}{}\n    // Queue used to implement BFS, using slice to simulate queue\n    queue := make([]Vertex, 0)\n    queue = append(queue, startVet)\n    // Starting from vertex vet, loop until all vertices are visited\n    for len(queue) > 0 {\n        // Dequeue the front vertex\n        vet := queue[0]\n        queue = queue[1:]\n        // Record visited vertex\n        res = append(res, vet)\n        // Traverse all adjacent vertices of this vertex\n        for _, adjVet := range g.adjList[vet] {\n            _, isExist := visited[adjVet]\n            // Only enqueue unvisited vertices\n            if !isExist {\n                queue = append(queue, adjVet)\n                visited[adjVet] = struct{}{}\n            }\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
    graph_bfs.swift
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Vertex traversal sequence\n    var res: [Vertex] = []\n    // Hash set for recording vertices that have been visited\n    var visited: Set<Vertex> = [startVet]\n    // Queue used to implement BFS\n    var que: [Vertex] = [startVet]\n    // Starting from vertex vet, loop until all vertices are visited\n    while !que.isEmpty {\n        let vet = que.removeFirst() // Dequeue the front vertex\n        res.append(vet) // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for adjVet in graph.adjList[vet] ?? [] {\n            if visited.contains(adjVet) {\n                continue // Skip vertices that have been visited\n            }\n            que.append(adjVet) // Only enqueue unvisited vertices\n            visited.insert(adjVet) // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
    graph_bfs.js
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphBFS(graph, startVet) {\n    // Vertex traversal sequence\n    const res = [];\n    // Hash set for recording vertices that have been visited\n    const visited = new Set();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    const que = [startVet];\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.length) {\n        const vet = que.shift(); // Dequeue the front vertex\n        res.push(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Skip vertices that have been visited\n            }\n            que.push(adjVet); // Only enqueue unvisited vertices\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.ts
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Vertex traversal sequence\n    const res: Vertex[] = [];\n    // Hash set for recording vertices that have been visited\n    const visited: Set<Vertex> = new Set();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    const que = [startVet];\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.length) {\n        const vet = que.shift(); // Dequeue the front vertex\n        res.push(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Skip vertices that have been visited\n            }\n            que.push(adjVet); // Only enqueue unvisited\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.dart
    /* Breadth-first traversal */\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n  // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  // Vertex traversal sequence\n  List<Vertex> res = [];\n  // Hash set for recording vertices that have been visited\n  Set<Vertex> visited = {};\n  visited.add(startVet);\n  // Queue used to implement BFS\n  Queue<Vertex> que = Queue();\n  que.add(startVet);\n  // Starting from vertex vet, loop until all vertices are visited\n  while (que.isNotEmpty) {\n    Vertex vet = que.removeFirst(); // Dequeue the front vertex\n    res.add(vet); // Record visited vertex\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex adjVet in graph.adjList[vet]!) {\n      if (visited.contains(adjVet)) {\n        continue; // Skip vertices that have been visited\n      }\n      que.add(adjVet); // Only enqueue unvisited vertices\n      visited.add(adjVet); // Mark this vertex as visited\n    }\n  }\n  // Return vertex traversal sequence\n  return res;\n}\n
    graph_bfs.rs
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Vertex traversal sequence\n    let mut res = vec![];\n    // Hash set for recording vertices that have been visited\n    let mut visited = HashSet::new();\n    visited.insert(start_vet);\n    // Queue used to implement BFS\n    let mut que = VecDeque::new();\n    que.push_back(start_vet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while let Some(vet) = que.pop_front() {\n        res.push(vet); // Record visited vertex\n\n        // Traverse all adjacent vertices of this vertex\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; // Skip vertices that have been visited\n                }\n                que.push_back(adj_vet); // Only enqueue unvisited vertices\n                visited.insert(adj_vet); // Mark this vertex as visited\n            }\n        }\n    }\n    // Return vertex traversal sequence\n    res\n}\n
    graph_bfs.c
    /* Node queue structure */\ntypedef struct {\n    Vertex *vertices[MAX_SIZE];\n    int front, rear, size;\n} Queue;\n\n/* Constructor */\nQueue *newQueue() {\n    Queue *q = (Queue *)malloc(sizeof(Queue));\n    q->front = q->rear = q->size = 0;\n    return q;\n}\n\n/* Check if the queue is empty */\nint isEmpty(Queue *q) {\n    return q->size == 0;\n}\n\n/* Enqueue operation */\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/* Dequeue operation */\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/* Check if vertex has been visited */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n    // Traverse to find node using O(n) time\n    for (int i = 0; i < size; i++) {\n        if (visited[i] == vet)\n            return 1;\n    }\n    return 0;\n}\n\n/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n    // Queue used to implement BFS\n    Queue *queue = newQueue();\n    enqueue(queue, startVet);\n    visited[(*visitedSize)++] = startVet;\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!isEmpty(queue)) {\n        Vertex *vet = dequeue(queue); // Dequeue the front vertex\n        res[(*resSize)++] = vet;      // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        AdjListNode *node = findNode(graph, vet);\n        while (node != NULL) {\n            // Skip vertices that have been visited\n            if (!isVisited(visited, *visitedSize, node->vertex)) {\n                enqueue(queue, node->vertex);             // Only enqueue unvisited vertices\n                visited[(*visitedSize)++] = node->vertex; // Mark this vertex as visited\n            }\n            node = node->next;\n        }\n    }\n    // Free memory\n    free(queue);\n}\n
    graph_bfs.kt
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {\n    // Vertex traversal sequence\n    val res = mutableListOf<Vertex?>()\n    // Hash set for recording vertices that have been visited\n    val visited = HashSet<Vertex>()\n    visited.add(startVet)\n    // Queue used to implement BFS\n    val que = LinkedList<Vertex>()\n    que.offer(startVet)\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.isEmpty()) {\n        val vet = que.poll() // Dequeue the front vertex\n        res.add(vet)         // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (adjVet in graph.adjList[vet]!!) {\n            if (visited.contains(adjVet))\n                continue        // Skip vertices that have been visited\n            que.offer(adjVet)   // Only enqueue unvisited vertices\n            visited.add(adjVet) // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
    graph_bfs.rb
    ### Breadth-first traversal ###\ndef graph_bfs(graph, start_vet)\n  # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  # Vertex traversal sequence\n  res = []\n  # Hash set for recording vertices that have been visited\n  visited = Set.new([start_vet])\n  # Queue used to implement BFS\n  que = [start_vet]\n  # Starting from vertex vet, loop until all vertices are visited\n  while que.length > 0\n    vet = que.shift # Dequeue the front vertex\n    res << vet # Record visited vertex\n    # Traverse all adjacent vertices of this vertex\n    for adj_vet in graph.adj_list[vet]\n      next if visited.include?(adj_vet) # Skip vertices that have been visited\n      que << adj_vet # Only enqueue unvisited vertices\n      visited.add(adj_vet) # Mark this vertex as visited\n    end\n  end\n  # Return vertex traversal sequence\n  res\nend\n

    The code is relatively abstract; it is recommended to refer to Figure 9-10 to deepen understanding.

    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 9-10   Steps of breadth-first search of a graph

    Is the breadth-first traversal sequence unique?

    Not unique. Breadth-first search only requires traversing in a \"near to far\" order, and the traversal order of vertices at the same distance can be arbitrarily shuffled. Taking Figure 9-10 as an example, the visit order of vertices \\(1\\) and \\(3\\) can be swapped, as can the visit order of vertices \\(2\\), \\(4\\), and \\(6\\).

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2-complexity-analysis","level":3,"title":"2.   Complexity Analysis","text":"

    Time complexity: All vertices will be enqueued and dequeued once, using \\(O(|V|)\\) time; in the process of traversing adjacent vertices, since it is an undirected graph, all edges will be visited \\(2\\) times, using \\(O(2|E|)\\) time; overall using \\(O(|V| + |E|)\\) time.

    Space complexity: The list res, hash set visited, and queue que can contain at most \\(|V|\\) vertices, using \\(O(|V|)\\) space.

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932-depth-first-search","level":2,"title":"9.3.2   Depth-First Search","text":"

    Depth-first search is a traversal method that prioritizes going as far as possible, then backtracks when no path remains. As shown in Figure 9-11, starting from the top-left vertex, visit an adjacent vertex of the current vertex, continuing until reaching a dead end, then return and continue going as far as possible before returning again, and so on, until all vertices have been traversed.

    Figure 9-11   Depth-first search of a graph

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1-algorithm-implementation_1","level":3,"title":"1.   Algorithm Implementation","text":"

    This \"go as far as possible then return\" algorithm paradigm is typically implemented using recursion. Similar to breadth-first search, in depth-first search we also need a hash set visited to record visited vertices and avoid revisiting.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py
    def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n    \"\"\"Depth-first traversal helper function\"\"\"\n    res.append(vet)  # Record visited vertex\n    visited.add(vet)  # Mark this vertex as visited\n    # Traverse all adjacent vertices of this vertex\n    for adjVet in graph.adj_list[vet]:\n        if adjVet in visited:\n            continue  # Skip vertices that have been visited\n        # Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Depth-first traversal\"\"\"\n    # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n    # Vertex traversal sequence\n    res = []\n    # Hash set for recording vertices that have been visited\n    visited = set[Vertex]()\n    dfs(graph, visited, res, start_vet)\n    return res\n
    graph_dfs.cpp
    /* Depth-first traversal helper function */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n    res.push_back(vet);   // Record visited vertex\n    visited.emplace(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex *adjVet : graph.adjList[vet]) {\n        if (visited.count(adjVet))\n            continue; // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n    // Vertex traversal sequence\n    vector<Vertex *> res;\n    // Hash set for recording vertices that have been visited\n    unordered_set<Vertex *> visited;\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.java
    /* Depth-first traversal helper function */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.add(vet);     // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex adjVet : graph.adjList.get(vet)) {\n        if (visited.contains(adjVet))\n            continue; // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = new ArrayList<>();\n    // Hash set for recording vertices that have been visited\n    Set<Vertex> visited = new HashSet<>();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.cs
    /* Depth-first traversal helper function */\nvoid DFS(GraphAdjList graph, HashSet<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.Add(vet);     // Record visited vertex\n    visited.Add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    foreach (Vertex adjVet in graph.adjList[vet]) {\n        if (visited.Contains(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        DFS(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = [];\n    // Hash set for recording vertices that have been visited\n    HashSet<Vertex> visited = [];\n    DFS(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.go
    /* Depth-first traversal helper function */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n    // append operation returns a new reference, must reassign original reference to new slice's reference\n    *res = append(*res, vet)\n    visited[vet] = struct{}{}\n    // Traverse all adjacent vertices of this vertex\n    for _, adjVet := range g.adjList[vet] {\n        _, isExist := visited[adjVet]\n        // Recursively visit adjacent vertices\n        if !isExist {\n            dfs(g, visited, res, adjVet)\n        }\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Vertex traversal sequence\n    res := make([]Vertex, 0)\n    // Hash set for recording vertices that have been visited\n    visited := make(map[Vertex]struct{})\n    dfs(g, visited, &res, startVet)\n    // Return vertex traversal sequence\n    return res\n}\n
    graph_dfs.swift
    /* Depth-first traversal helper function */\nfunc dfs(graph: GraphAdjList, visited: inout Set<Vertex>, res: inout [Vertex], vet: Vertex) {\n    res.append(vet) // Record visited vertex\n    visited.insert(vet) // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for adjVet in graph.adjList[vet] ?? [] {\n        if visited.contains(adjVet) {\n            continue // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph: graph, visited: &visited, res: &res, vet: adjVet)\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Vertex traversal sequence\n    var res: [Vertex] = []\n    // Hash set for recording vertices that have been visited\n    var visited: Set<Vertex> = []\n    dfs(graph: graph, visited: &visited, res: &res, vet: startVet)\n    return res\n}\n
    graph_dfs.js
    /* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction dfs(graph, visited, res, vet) {\n    res.push(vet); // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphDFS(graph, startVet) {\n    // Vertex traversal sequence\n    const res = [];\n    // Hash set for recording vertices that have been visited\n    const visited = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.ts
    /* Depth-first traversal helper function */\nfunction dfs(\n    graph: GraphAdjList,\n    visited: Set<Vertex>,\n    res: Vertex[],\n    vet: Vertex\n): void {\n    res.push(vet); // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Vertex traversal sequence\n    const res: Vertex[] = [];\n    // Hash set for recording vertices that have been visited\n    const visited: Set<Vertex> = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.dart
    /* Depth-first traversal helper function */\nvoid dfs(\n  GraphAdjList graph,\n  Set<Vertex> visited,\n  List<Vertex> res,\n  Vertex vet,\n) {\n  res.add(vet); // Record visited vertex\n  visited.add(vet); // Mark this vertex as visited\n  // Traverse all adjacent vertices of this vertex\n  for (Vertex adjVet in graph.adjList[vet]!) {\n    if (visited.contains(adjVet)) {\n      continue; // Skip vertices that have been visited\n    }\n    // Recursively visit adjacent vertices\n    dfs(graph, visited, res, adjVet);\n  }\n}\n\n/* Depth-first traversal */\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n  // Vertex traversal sequence\n  List<Vertex> res = [];\n  // Hash set for recording vertices that have been visited\n  Set<Vertex> visited = {};\n  dfs(graph, visited, res, startVet);\n  return res;\n}\n
    graph_dfs.rs
    /* Depth-first traversal helper function */\nfn dfs(graph: &GraphAdjList, visited: &mut HashSet<Vertex>, res: &mut Vec<Vertex>, vet: Vertex) {\n    res.push(vet); // Record visited vertex\n    visited.insert(vet); // Mark this vertex as visited\n                         // Traverse all adjacent vertices of this vertex\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; // Skip vertices that have been visited\n            }\n            // Recursively visit adjacent vertices\n            dfs(graph, visited, res, adj_vet);\n        }\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Vertex traversal sequence\n    let mut res = vec![];\n    // Hash set for recording vertices that have been visited\n    let mut visited = HashSet::new();\n    dfs(&graph, &mut visited, &mut res, start_vet);\n\n    res\n}\n
    graph_dfs.c
    /* Check if vertex has been visited */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n    // Traverse to find node using O(n) time\n    for (int i = 0; i < size; i++) {\n        if (res[i] == vet) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/* Depth-first traversal helper function */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n    // Record visited vertex\n    res[(*resSize)++] = vet;\n    // Traverse all adjacent vertices of this vertex\n    AdjListNode *node = findNode(graph, vet);\n    while (node != NULL) {\n        // Skip vertices that have been visited\n        if (!isVisited(res, *resSize, node->vertex)) {\n            // Recursively visit adjacent vertices\n            dfs(graph, res, resSize, node->vertex);\n        }\n        node = node->next;\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n    dfs(graph, res, resSize, startVet);\n}\n
    graph_dfs.kt
    /* Depth-first traversal helper function */\nfun dfs(\n    graph: GraphAdjList,\n    visited: MutableSet<Vertex?>,\n    res: MutableList<Vertex?>,\n    vet: Vertex?\n) {\n    res.add(vet)     // Record visited vertex\n    visited.add(vet) // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (adjVet in graph.adjList[vet]!!) {\n        if (visited.contains(adjVet))\n            continue  // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet)\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {\n    // Vertex traversal sequence\n    val res = mutableListOf<Vertex?>()\n    // Hash set for recording vertices that have been visited\n    val visited = HashSet<Vertex?>()\n    dfs(graph, visited, res, startVet)\n    return res\n}\n
    graph_dfs.rb
    ### Depth-first traversal helper function ###\ndef dfs(graph, visited, res, vet)\n  res << vet # Record visited vertex\n  visited.add(vet) # Mark this vertex as visited\n  # Traverse all adjacent vertices of this vertex\n  for adj_vet in graph.adj_list[vet]\n    next if visited.include?(adj_vet) # Skip vertices that have been visited\n    # Recursively visit adjacent vertices\n    dfs(graph, visited, res, adj_vet)\n  end\nend\n\n### Depth-first traversal ###\ndef graph_dfs(graph, start_vet)\n  # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  # Vertex traversal sequence\n  res = []\n  # Hash set for recording vertices that have been visited\n  visited = Set.new\n  dfs(graph, visited, res, start_vet)\n  res\nend\n

    The algorithm flow of depth-first search is shown in Figure 9-12.

    • Straight dashed lines represent downward recursion, indicating that a new recursive method has been initiated to visit a new vertex.
    • Curved dashed lines represent upward backtracking, indicating that this recursive call has returned to the point where it was made.

    To deepen understanding, it is recommended to combine Figure 9-12 with the code to mentally simulate (or draw out) the entire DFS process, including when each recursive call begins and when it returns.

    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 9-12   Steps of depth-first search of a graph

    Is the depth-first traversal sequence unique?

    Similar to breadth-first search, depth-first traversal sequences are also not unique. Given a vertex, any exploration direction may be chosen first; that is, the order of adjacent vertices can be arbitrarily rearranged and still constitute depth-first search.

    Taking tree traversal as an example, \"root \\(\\rightarrow\\) left \\(\\rightarrow\\) right\", \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\", and \"left \\(\\rightarrow\\) right \\(\\rightarrow\\) root\" correspond to pre-order, in-order, and post-order traversals, respectively. They represent three different traversal priorities, yet all three belong to depth-first search.

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2-complexity-analysis_1","level":3,"title":"2.   Complexity Analysis","text":"

    Time complexity: All vertices will be visited \\(1\\) time, using \\(O(|V|)\\) time; all edges will be visited \\(2\\) times, using \\(O(2|E|)\\) time; overall using \\(O(|V| + |E|)\\) time.

    Space complexity: The list res and hash set visited can contain at most \\(|V|\\) vertices, and the maximum recursion depth is \\(|V|\\), therefore using \\(O(|V|)\\) space.

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   Summary","text":"","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_graph/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Graphs consist of vertices and edges and can be represented as a set of vertices and a set of edges.
    • Compared with the linear relationships modeled by linked lists and the divide-and-conquer relationships modeled by trees, the network relationships modeled by graphs offer much greater flexibility and are therefore more complex.
    • In directed graphs, edges have direction; in connected graphs, every vertex is reachable from any other vertex; and in weighted graphs, each edge carries a weight.
    • Adjacency matrices use matrices to represent graphs, where each row (column) represents a vertex, and matrix elements represent edges, using \\(1\\) or \\(0\\) to indicate whether two vertices have an edge or not. Adjacency matrices are highly efficient for addition, deletion, lookup, and modification operations, but consume significant space.
    • Adjacency lists use multiple linked lists to represent a graph: the \\(i\\)-th linked list corresponds to vertex \\(i\\) and stores all vertices adjacent to it. Compared with adjacency matrices, adjacency lists use less space, but edge lookups are less efficient because the linked list must be traversed.
    • When linked lists in adjacency lists become too long, they can be converted to red-black trees or hash tables, thereby improving lookup efficiency.
    • From an algorithmic perspective, adjacency matrices embody \"trading space for time\", while adjacency lists embody \"trading time for space\".
    • Graphs can be used to model various real-world systems, such as social networks and subway lines.
    • Trees are a special case of graphs, and tree traversal is a special case of graph traversal.
    • Breadth-first search in graphs explores from near to far, expanding layer by layer, and is typically implemented with a queue.
    • Depth-first search in graphs follows a path as deep as possible and backtracks when it can go no farther, and is commonly implemented with recursion.
    ","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Is a path defined as a sequence of vertices or a sequence of edges?

    The definitions in different language versions of Wikipedia are inconsistent: the English version states \"a path is a sequence of edges\", while the Chinese version states \"a path is a sequence of vertices\". The following is the original English text: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

    In this text, a path is viewed as a sequence of edges, not a sequence of vertices. This is because there may be multiple edges connecting two vertices, in which case each edge corresponds to a path.

    Q: In a disconnected graph, will there be unreachable vertices?

    In a disconnected graph, if you start from one vertex, at least one other vertex will be unreachable. To traverse a disconnected graph, you need multiple starting points so that all connected components are covered.

    Q: In an adjacency list, is there any required ordering for the vertices adjacent to a given vertex?

    They can appear in any order. In practice, however, they may need to be sorted according to specific rules, such as the order in which vertices were added or the order of vertex values, which helps when quickly finding a vertex with some extreme value.

    ","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"Chapter 15.   Greedy","text":"

    Abstract

    Sunflowers turn toward the sun, always seeking the fullest growth possible.

    Through successive simple choices, greedy strategies gradually lead to the optimal solution.

    ","path":["Chapter 15. Greedy","Chapter 15.   Greedy"],"tags":[]},{"location":"chapter_greedy/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 15.1   Greedy Algorithm
    • 15.2   Fractional Knapsack Problem
    • 15.3   Maximum Capacity Problem
    • 15.4   Maximum Product Cutting Problem
    • 15.5   Summary
    ","path":["Chapter 15. Greedy","Chapter 15.   Greedy"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   Fractional Knapsack Problem","text":"

    Question

    Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can be selected only once, but a fraction of an item may be selected, with its value proportional to the selected weight. What is the maximum total value that can be placed in the knapsack under the capacity constraint? An example is shown in Figure 15-3.

    Figure 15-3   Example data for the fractional knapsack problem

    The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, with states including the current item \\(i\\) and capacity \\(c\\), and the goal being to maximize value under the limited knapsack capacity.

    The difference is that this problem allows selecting only a fraction of an item. As shown in Figure 15-4, we can split an item arbitrarily and compute its value in proportion to the selected weight.

    1. For item \\(i\\), its value per unit weight is \\(val[i-1] / wgt[i-1]\\), referred to as unit value.
    2. Suppose we put a portion of item \\(i\\) with weight \\(w\\) into the knapsack, then the value added to the knapsack is \\(w \\times val[i-1] / wgt[i-1]\\).

    Figure 15-4   Value of items per unit weight

    ","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

    Maximizing the total value in the knapsack essentially means prioritizing items with higher value per unit weight. From this observation, we can derive the greedy strategy shown in Figure 15-5.

    1. Sort items by unit value from high to low.
    2. Iterate through all items, greedily selecting the item with the highest unit value in each round.
    3. If the remaining knapsack capacity is insufficient, use a portion of the current item to fill the knapsack.

    Figure 15-5   Greedy strategy for the fractional knapsack problem

    ","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    We define an Item class so that items can be sorted by unit value. We then iterate through the sorted items greedily, stopping once the knapsack is full and returning the result:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py
    class Item:\n    \"\"\"Item\"\"\"\n\n    def __init__(self, w: int, v: int):\n        self.w = w  # Item weight\n        self.v = v  # Item value\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Fractional knapsack: Greedy algorithm\"\"\"\n    # Create item list with two attributes: weight, value\n    items = [Item(w, v) for w, v in zip(wgt, val)]\n    # Sort by unit value item.v / item.w from high to low\n    items.sort(key=lambda item: item.v / item.w, reverse=True)\n    # Loop for greedy selection\n    res = 0\n    for item in items:\n        if item.w <= cap:\n            # If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v\n            cap -= item.w\n        else:\n            # If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap\n            # No remaining capacity, so break out of the loop\n            break\n    return res\n
    fractional_knapsack.cpp
    /* Item */\nclass Item {\n  public:\n    int w; // Item weight\n    int v; // Item value\n\n    Item(int w, int v) : w(w), v(v) {\n    }\n};\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n    // Create item list with two attributes: weight, value\n    vector<Item> items;\n    for (int i = 0; i < wgt.size(); i++) {\n        items.push_back(Item(wgt[i], val[i]));\n    }\n    // Sort by unit value item.v / item.w from high to low\n    sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n    // Loop for greedy selection\n    double res = 0;\n    for (auto &item : items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double)item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.java
    /* Item */\nclass Item {\n    int w; // Item weight\n    int v; // Item value\n\n    public Item(int w, int v) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Create item list with two attributes: weight, value\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    // Sort by unit value item.v / item.w from high to low\n    Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n    // Loop for greedy selection\n    double res = 0;\n    for (Item item : items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double) item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.cs
    /* Item */\nclass Item(int w, int v) {\n    public int w = w; // Item weight\n    public int v = v; // Item value\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Create item list with two attributes: weight, value\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    // Sort by unit value item.v / item.w from high to low\n    Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));\n    // Loop for greedy selection\n    double res = 0;\n    foreach (Item item in items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double)item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.go
    /* Item */\ntype Item struct {\n    w int // Item weight\n    v int // Item value\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n    // Create item list with two attributes: weight, value\n    items := make([]Item, len(wgt))\n    for i := 0; i < len(wgt); i++ {\n        items[i] = Item{wgt[i], val[i]}\n    }\n    // Sort by unit value item.v / item.w from high to low\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    // Loop for greedy selection\n    res := 0.0\n    for _, item := range items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += float64(item.v)\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += float64(item.v) / float64(item.w) * float64(cap)\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
    fractional_knapsack.swift
    /* Item */\nclass Item {\n    var w: Int // Item weight\n    var v: Int // Item value\n\n    init(w: Int, v: Int) {\n        self.w = w\n        self.v = v\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {\n    // Create item list with two attributes: weight, value\n    var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n    // Sort by unit value item.v / item.w from high to low\n    items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }\n    // Loop for greedy selection\n    var res = 0.0\n    var cap = cap\n    for item in items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += Double(item.v)\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += Double(item.v) / Double(item.w) * Double(cap)\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
    fractional_knapsack.js
    /* Item */\nclass Item {\n    constructor(w, v) {\n        this.w = w; // Item weight\n        this.v = v; // Item value\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunction fractionalKnapsack(wgt, val, cap) {\n    // Create item list with two attributes: weight, value\n    const items = wgt.map((w, i) => new Item(w, val[i]));\n    // Sort by unit value item.v / item.w from high to low\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Loop for greedy selection\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.ts
    /* Item */\nclass Item {\n    w: number; // Item weight\n    v: number; // Item value\n\n    constructor(w: number, v: number) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n    // Create item list with two attributes: weight, value\n    const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));\n    // Sort by unit value item.v / item.w from high to low\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Loop for greedy selection\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.dart
    /* Item */\nclass Item {\n  int w; // Item weight\n  int v; // Item value\n\n  Item(this.w, this.v);\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(List<int> wgt, List<int> val, int cap) {\n  // Create item list with two attributes: weight, value\n  List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));\n  // Sort by unit value item.v / item.w from high to low\n  items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));\n  // Loop for greedy selection\n  double res = 0;\n  for (Item item in items) {\n    if (item.w <= cap) {\n      // If remaining capacity is sufficient, put the entire current item into the knapsack\n      res += item.v;\n      cap -= item.w;\n    } else {\n      // If remaining capacity is insufficient, put part of the current item into the knapsack\n      res += item.v / item.w * cap;\n      // No remaining capacity, so break out of the loop\n      break;\n    }\n  }\n  return res;\n}\n
    fractional_knapsack.rs
    /* Item */\nstruct Item {\n    w: i32, // Item weight\n    v: i32, // Item value\n}\n\nimpl Item {\n    fn new(w: i32, v: i32) -> Self {\n        Self { w, v }\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {\n    // Create item list with two attributes: weight, value\n    let mut items = wgt\n        .iter()\n        .zip(val.iter())\n        .map(|(&w, &v)| Item::new(w, v))\n        .collect::<Vec<Item>>();\n    // Sort by unit value item.v / item.w from high to low\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    // Loop for greedy selection\n    let mut res = 0.0;\n    for item in &items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v as f64;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += item.v as f64 / item.w as f64 * cap as f64;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    res\n}\n
    fractional_knapsack.c
    /* Item */\ntypedef struct {\n    int w; // Item weight\n    int v; // Item value\n} Item;\n\n/* Fractional knapsack: Greedy algorithm */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n    // Create item list with two attributes: weight, value\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    // Sort by unit value item.v / item.w from high to low\n    qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n    // Loop for greedy selection\n    float res = 0.0;\n    for (int i = 0; i < itemCount; i++) {\n        if (items[i].w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += items[i].v;\n            cap -= items[i].w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\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
    /* Item */\nclass Item(\n    val w: Int, // Item\n    val v: Int  // Item value\n)\n\n/* Fractional knapsack: Greedy algorithm */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n    // Create item list with two attributes: weight, value\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    // Sort by unit value item.v / item.w from high to low\n    items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }\n    // Loop for greedy selection\n    var res = 0.0\n    for (item in items) {\n        if (item!!.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += item.v.toDouble() / item.w * cap\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
    fractional_knapsack.rb
    ### Item ###\nclass Item\n  attr_accessor :w # Item weight\n  attr_accessor :v # Item value\n\n  def initialize(w, v)\n    @w = w\n    @v = v\n  end\nend\n\n### Fractional knapsack: greedy ###\ndef fractional_knapsack(wgt, val, cap)\n  # Create item list with two attributes: weight, value\n  items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n  # Sort by unit value item.v / item.w from high to low\n  items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }\n  # Loop for greedy selection\n  res = 0\n  for item in items\n    if item.w <= cap\n      # If remaining capacity is sufficient, put the entire current item into the knapsack\n      res += item.v\n      cap -= item.w\n    else\n      # If remaining capacity is insufficient, put part of the current item into the knapsack\n      res += (item.v.to_f / item.w) * cap\n      # No remaining capacity, so break out of the loop\n      break\n    end\n  end\n  res\nend\n

    Built-in sorting algorithms usually take \\(O(n \\log n)\\) time, and their space complexity is usually \\(O(\\log n)\\) or \\(O(n)\\), depending on the specific implementation of the programming language.

    Apart from sorting, in the worst case the entire item list needs to be traversed, therefore the time complexity is \\(O(n)\\), where \\(n\\) is the number of items.

    Since an Item object list is initialized, the space complexity is \\(O(n)\\).

    ","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

    We use proof by contradiction. Suppose item \\(x\\) has the highest unit value, and some algorithm produces an optimal value res, but the resulting solution does not include item \\(x\\).

    Now remove one unit of weight from any item in the knapsack and replace it with one unit of weight from item \\(x\\). Since item \\(x\\) has the highest unit value, the total value after the replacement must be greater than res. This contradicts the assumption that res is optimal, proving that any optimal solution must include item \\(x\\).

    We can construct the same contradiction for the other items in the solution as well. In summary, items with higher unit value are always the better choice, which proves that the greedy strategy is effective.

    As shown in Figure 15-6, if we treat item weight and unit value as the horizontal and vertical axes of a two-dimensional chart, then the fractional knapsack problem can be viewed as \"finding the maximum area enclosed within a bounded interval on the horizontal axis.\" This analogy helps explain the effectiveness of the greedy strategy from a geometric perspective.

    Figure 15-6   Geometric representation of the fractional knapsack problem

    ","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   Greedy Algorithm","text":"

    Greedy algorithm is a common approach to solving optimization problems. Its basic idea is to choose the option that appears best at each decision stage, that is, to greedily make locally optimal decisions in the hope of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely used in many practical problems.

    Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as both relying on the optimal substructure property, but they work differently.

    • Dynamic programming considers all previous decisions when making the current decision, and uses solutions to past subproblems to construct the solution to the current subproblem.
    • Greedy algorithms do not consider past decisions, but instead make greedy choices moving forward, continually reducing the problem size until the problem is solved.

    We will first understand how greedy algorithms work through the example problem \"coin change.\" This problem was already introduced in the \"Complete Knapsack Problem\" chapter, so it should already be familiar to you.

    Question

    Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type is \\(coins[i - 1]\\), a target amount \\(amt\\), and an unlimited number of coins of each type, what is the minimum number of coins needed to make up the target amount? If the target amount cannot be made up, return \\(-1\\).

    The greedy strategy for this problem is shown in Figure 15-1. Given a target amount, we greedily choose the coin that does not exceed it and is closest to it, repeating this step until the target amount is made up.

    Figure 15-1   Greedy strategy for coin change

    The implementation code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py
    def coin_change_greedy(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Greedy algorithm\"\"\"\n    # Assume coins list is sorted\n    i = len(coins) - 1\n    count = 0\n    # Loop to make greedy choices until no remaining amount\n    while amt > 0:\n        # Find the coin that is less than and closest to the remaining amount\n        while i > 0 and coins[i] > amt:\n            i -= 1\n        # Choose coins[i]\n        amt -= coins[i]\n        count += 1\n    # If no feasible solution is found, return -1\n    return count if amt == 0 else -1\n
    coin_change_greedy.cpp
    /* Coin change: Greedy algorithm */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.size() - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.java
    /* Coin change: Greedy algorithm */\nint coinChangeGreedy(int[] coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.length - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.cs
    /* Coin change: Greedy algorithm */\nint CoinChangeGreedy(int[] coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.Length - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.go
    /* Coin change: Greedy algorithm */\nfunc coinChangeGreedy(coins []int, amt int) int {\n    // Assume coins list is sorted\n    i := len(coins) - 1\n    count := 0\n    // Loop to make greedy choices until no remaining amount\n    for amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        for i > 0 && coins[i] > amt {\n            i--\n        }\n        // Choose coins[i]\n        amt -= coins[i]\n        count++\n    }\n    // If no feasible solution is found, return -1\n    if amt != 0 {\n        return -1\n    }\n    return count\n}\n
    coin_change_greedy.swift
    /* Coin change: Greedy algorithm */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -> Int {\n    // Assume coins list is sorted\n    var i = coins.count - 1\n    var count = 0\n    var amt = amt\n    // Loop to make greedy choices until no remaining amount\n    while amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        while i > 0 && coins[i] > amt {\n            i -= 1\n        }\n        // Choose coins[i]\n        amt -= coins[i]\n        count += 1\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1\n}\n
    coin_change_greedy.js
    /* Coin change: Greedy algorithm */\nfunction coinChangeGreedy(coins, amt) {\n    // Assume coins array is sorted\n    let i = coins.length - 1;\n    let count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt === 0 ? count : -1;\n}\n
    coin_change_greedy.ts
    /* Coin change: Greedy algorithm */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n    // Assume coins array is sorted\n    let i = coins.length - 1;\n    let count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt === 0 ? count : -1;\n}\n
    coin_change_greedy.dart
    /* Coin change: Greedy algorithm */\nint coinChangeGreedy(List<int> coins, int amt) {\n  // Assume coins list is sorted\n  int i = coins.length - 1;\n  int count = 0;\n  // Loop to make greedy choices until no remaining amount\n  while (amt > 0) {\n    // Find the coin that is less than and closest to the remaining amount\n    while (i > 0 && coins[i] > amt) {\n      i--;\n    }\n    // Choose coins[i]\n    amt -= coins[i];\n    count++;\n  }\n  // If no feasible solution is found, return -1\n  return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.rs
    /* Coin change: Greedy algorithm */\nfn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {\n    // Assume coins list is sorted\n    let mut i = coins.len() - 1;\n    let mut count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        while i > 0 && coins[i] > amt {\n            i -= 1;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count += 1;\n    }\n    // If no feasible solution is found, return -1\n    if amt == 0 {\n        count\n    } else {\n        -1\n    }\n}\n
    coin_change_greedy.c
    /* Coin change: Greedy algorithm */\nint coinChangeGreedy(int *coins, int size, int amt) {\n    // Assume coins list is sorted\n    int i = size - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.kt
    /* Coin change: Greedy algorithm */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n    // Assume coins list is sorted\n    var am = amt\n    var i = coins.size - 1\n    var count = 0\n    // Loop to make greedy choices until no remaining amount\n    while (am > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > am) {\n            i--\n        }\n        // Choose coins[i]\n        am -= coins[i]\n        count++\n    }\n    // If no feasible solution is found, return -1\n    return if (am == 0) count else -1\n}\n
    coin_change_greedy.rb
    ### Coin change: greedy ###\ndef coin_change_greedy(coins, amt)\n  # Assume coins list is sorted\n  i = coins.length - 1\n  count = 0\n  # Loop to make greedy choices until no remaining amount\n  while amt > 0\n    # Find the coin that is less than and closest to the remaining amount\n    while i > 0 && coins[i] > amt\n      i -= 1\n    end\n    # Choose coins[i]\n    amt -= coins[i]\n    count += 1\n  end\n  # Return -1 if no solution found\n  amt == 0 ? count : -1\nend\n

    You may find yourself exclaiming, \"So clean!\" The greedy algorithm solves the coin change problem in only about ten lines of code.

    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511-advantages-and-limitations-of-greedy-algorithms","level":2,"title":"15.1.1   Advantages and Limitations of Greedy Algorithms","text":"

    Greedy algorithms are not only straightforward to apply and easy to implement, but are also usually very efficient. In the code above, if the smallest coin denomination is \\(\\min(coins)\\), the greedy selection loop runs at most \\(amt / \\min(coins)\\) times, giving a time complexity of \\(O(amt / \\min(coins))\\). This is an order of magnitude lower than the time complexity of the dynamic programming solution, \\(O(n \\times amt)\\).

    However, for some coin denomination sets, greedy algorithms cannot find the optimal solution. Figure 15-2 shows two examples.

    • Positive example \\(coins = [1, 5, 10, 20, 50, 100]\\): With this coin set, the greedy algorithm can find the optimal solution for any \\(amt\\).
    • Counterexample \\(coins = [1, 20, 50]\\): Suppose \\(amt = 60\\). The greedy algorithm can only find the combination \\(50 + 1 \\times 10\\), using \\(11\\) coins in total, whereas dynamic programming can find the optimal solution \\(20 + 20 + 20\\) using only \\(3\\) coins.
    • Counterexample \\(coins = [1, 49, 50]\\): Suppose \\(amt = 98\\). The greedy algorithm can only find the combination \\(50 + 1 \\times 48\\), using \\(49\\) coins in total, whereas dynamic programming can find the optimal solution \\(49 + 49\\) using only \\(2\\) coins.

    Figure 15-2   Examples where greedy algorithms cannot find the optimal solution

    In other words, for the coin change problem, greedy algorithms cannot guarantee a globally optimal solution and may even produce very poor results. This problem is better solved with dynamic programming.

    In general, greedy algorithms are applicable in the following two situations.

    1. The optimal solution can be guaranteed: In this case, greedy algorithms are often the best choice because they tend to be more efficient than backtracking and dynamic programming.
    2. An approximately optimal solution can be found: Greedy algorithms are also useful in this case. For many complex problems, finding the global optimal solution is very difficult, so efficiently finding a suboptimal solution is already a very good outcome.
    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512-characteristics-of-greedy-algorithms","level":2,"title":"15.1.2   Characteristics of Greedy Algorithms","text":"

    So the question arises: what kind of problems are suitable for solving with greedy algorithms? Or in other words, under what conditions can greedy algorithms guarantee finding the optimal solution?

    Compared to dynamic programming, the conditions for using greedy algorithms are stricter, mainly focusing on two properties of the problem.

    • Greedy choice property: Only when locally optimal choices can always lead to a globally optimal solution can greedy algorithms guarantee obtaining the optimal solution.
    • Optimal substructure: The optimal solution to the original problem contains the optimal solutions to subproblems.

    Optimal substructure has already been introduced in the \"Dynamic Programming\" chapter, so we won't elaborate on it here. It's worth noting that the optimal substructure of some problems is not obvious, but they can still be solved using greedy algorithms.

    We mainly explore methods for determining the greedy choice property. Although its description seems relatively simple, in practice, for many problems, proving the greedy choice property is not easy.

    For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving that it holds is much harder. If asked, under what conditions can a coin set be solved using a greedy algorithm? We often can only rely on intuition or examples to give a vague answer, and it is difficult to provide a rigorous mathematical proof.

    Quote

    There is a paper that presents an \\(O(n^3)\\) algorithm for determining whether a coin set can be solved optimally by a greedy algorithm for any amount.

    Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.

    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513-steps-for-solving-problems-with-greedy-algorithms","level":2,"title":"15.1.3   Steps for Solving Problems with Greedy Algorithms","text":"

    The general process for solving greedy problems can be divided into the following three steps.

    1. Problem analysis: Sort out and understand the characteristics of the problem, including state definitions, optimization objectives, and constraints. This step also appears in backtracking and dynamic programming.
    2. Determine the greedy strategy: Decide how to make a greedy choice at each step. This strategy should reduce the problem size step by step and ultimately solve the entire problem.
    3. Correctness proof: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical tools such as induction or proof by contradiction.

    Determining the greedy strategy is the core step in solving such problems, but it may not be easy in practice, mainly for the following reasons.

    • Greedy strategies vary greatly from problem to problem. For many problems, the greedy strategy is fairly intuitive and can be derived through rough reasoning and experimentation. For some complex problems, however, the greedy strategy may be deeply hidden, which strongly tests one's problem-solving experience and algorithmic ability.
    • Some greedy strategies are highly deceptive. We may confidently design a greedy strategy, write the solution code, and submit it, only to find that some test cases fail. This is because the designed greedy strategy is only \"partially correct,\" as exemplified by the coin change problem discussed above.

    To ensure correctness, we should give a rigorous mathematical proof of the greedy strategy, usually using proof by contradiction or mathematical induction.

    However, correctness proofs can also be difficult. If we have no clear direction, we usually resort to debugging against test cases, revising and validating the greedy strategy step by step.

    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514-typical-problems-solved-by-greedy-algorithms","level":2,"title":"15.1.4   Typical Problems Solved by Greedy Algorithms","text":"

    Greedy algorithms are often applied to optimization problems that satisfy greedy choice property and optimal substructure. Below are some typical greedy algorithm problems.

    • Coin change problem: With certain coin combinations, greedy algorithms can always obtain the optimal solution.
    • Interval scheduling problem: Suppose you have some tasks, each taking place during a period of time, and your goal is to complete as many tasks as possible. If you always choose the task that ends earliest, then the greedy algorithm can obtain the optimal solution.
    • Fractional knapsack problem: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), then the greedy algorithm can obtain the optimal solution in some cases.
    • Stock trading problem: Given a set of historical stock prices, you can make multiple trades, but if you already hold stocks, you cannot buy again before selling, and the goal is to obtain the maximum profit.
    • Huffman coding: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree and always merging the two nodes with the lowest frequency, the resulting Huffman tree has the minimum weighted path length (encoding length).
    • Dijkstra's algorithm: It is a greedy algorithm for solving the shortest path problem from a given source vertex to all other vertices.
    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   Max Capacity Problem","text":"

    Question

    Given an array \\(ht\\), where each element represents the height of a vertical partition. Any two partitions in the array, together with the space between them, can form a container.

    The capacity of the container equals the product of its height and width (that is, its area), where the height is determined by the shorter partition and the width is the difference between the array indices of the two partitions.

    Select two partitions in the array such that the capacity of the resulting container is maximized, and return that maximum capacity. An example is shown in Figure 15-7.

    Figure 15-7   Example data for the max capacity problem

    The container is formed by any two partitions, so the state of this problem is the indices of the two partitions, denoted by \\([i, j]\\).

    According to the problem statement, capacity equals height multiplied by width, where the height is determined by the shorter partition and the width is the difference between the array indices of the two partitions. Let the capacity be \\(cap[i, j]\\); then we obtain the following formula:

    \\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

    Let the array length be \\(n\\). Then the number of ways to choose two partitions (that is, the total number of states) is \\(C_n^2 = \\frac{n(n - 1)}{2}\\). The most straightforward approach is to exhaustively enumerate all states to find the maximum capacity, which has a time complexity of \\(O(n^2)\\).

    ","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

    This problem has a more efficient solution. As shown in Figure 15-8, consider a state \\([i, j]\\) where \\(i < j\\) and \\(ht[i] < ht[j]\\). In this case, \\(i\\) is the shorter partition and \\(j\\) is the taller partition.

    Figure 15-8   Initial state

    As shown in Figure 15-9, if we now move the taller partition \\(j\\) inward toward the shorter partition \\(i\\), the capacity will definitely decrease.

    This is because after moving the taller partition \\(j\\), the width \\(j-i\\) definitely decreases. Since the height is determined by the shorter partition, the height can only stay the same (\\(i\\) remains the shorter partition) or decrease (\\(j\\) becomes the shorter partition after being moved).

    Figure 15-9   State after moving the long partition inward

    Conversely, only by moving the shorter partition \\(i\\) inward can the capacity possibly increase. Although the width will definitely decrease, the height may increase (the moved partition at \\(i\\) may be taller). For example, in Figure 15-10, the area increases after moving the shorter partition.

    Figure 15-10   State after moving the short partition inward

    From this, we can derive the greedy strategy for this problem: initialize two pointers at the two ends, and in each round move the pointer corresponding to the shorter partition inward until the two pointers meet.

    Figure 15-11 shows the execution process of the greedy strategy.

    1. In the initial state, pointers \\(i\\) and \\(j\\) are at both ends of the array.
    2. Calculate the capacity of the current state \\(cap[i, j]\\), and update the maximum capacity.
    3. Compare the heights of partitions \\(i\\) and \\(j\\), and move the pointer corresponding to the shorter partition inward by one position.
    4. Repeat steps 2. and 3. until \\(i\\) and \\(j\\) meet.
    <1><2><3><4><5><6><7><8><9>

    Figure 15-11   Greedy process for the max capacity problem

    ","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    The code runs for at most \\(n\\) rounds, so the time complexity is \\(O(n)\\).

    Variables \\(i\\), \\(j\\), and \\(res\\) use only a constant amount of extra space, so the space complexity is \\(O(1)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py
    def max_capacity(ht: list[int]) -> int:\n    \"\"\"Max capacity: Greedy algorithm\"\"\"\n    # Initialize i, j to be at both ends of the array\n    i, j = 0, len(ht) - 1\n    # Initial max capacity is 0\n    res = 0\n    # Loop for greedy selection until the two boards meet\n    while i < j:\n        # Update max capacity\n        cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        # Move the shorter board inward\n        if ht[i] < ht[j]:\n            i += 1\n        else:\n            j -= 1\n    return res\n
    max_capacity.cpp
    /* Max capacity: Greedy algorithm */\nint maxCapacity(vector<int> &ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.size() - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = min(ht[i], ht[j]) * (j - i);\n        res = max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
    max_capacity.java
    /* Max capacity: Greedy algorithm */\nint maxCapacity(int[] ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.length - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
    max_capacity.cs
    /* Max capacity: Greedy algorithm */\nint MaxCapacity(int[] ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.Length - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = Math.Min(ht[i], ht[j]) * (j - i);\n        res = Math.Max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
    max_capacity.go
    /* Max capacity: Greedy algorithm */\nfunc maxCapacity(ht []int) int {\n    // Initialize i, j to be at both ends of the array\n    i, j := 0, len(ht)-1\n    // Initial max capacity is 0\n    res := 0\n    // Loop for greedy selection until the two boards meet\n    for i < j {\n        // Update max capacity\n        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n        res = int(math.Max(float64(res), float64(capacity)))\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
    max_capacity.swift
    /* Max capacity: Greedy algorithm */\nfunc maxCapacity(ht: [Int]) -> Int {\n    // Initialize i, j to be at both ends of the array\n    var i = ht.startIndex, j = ht.endIndex - 1\n    // Initial max capacity is 0\n    var res = 0\n    // Loop for greedy selection until the two boards meet\n    while i < j {\n        // Update max capacity\n        let cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i += 1\n        } else {\n            j -= 1\n        }\n    }\n    return res\n}\n
    max_capacity.js
    /* Max capacity: Greedy algorithm */\nfunction maxCapacity(ht) {\n    // Initialize i, j to be at both ends of the array\n    let i = 0,\n        j = ht.length - 1;\n    // Initial max capacity is 0\n    let res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        const cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
    max_capacity.ts
    /* Max capacity: Greedy algorithm */\nfunction maxCapacity(ht: number[]): number {\n    // Initialize i, j to be at both ends of the array\n    let i = 0,\n        j = ht.length - 1;\n    // Initial max capacity is 0\n    let res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
    max_capacity.dart
    /* Max capacity: Greedy algorithm */\nint maxCapacity(List<int> ht) {\n  // Initialize i, j to be at both ends of the array\n  int i = 0, j = ht.length - 1;\n  // Initial max capacity is 0\n  int res = 0;\n  // Loop for greedy selection until the two boards meet\n  while (i < j) {\n    // Update max capacity\n    int cap = min(ht[i], ht[j]) * (j - i);\n    res = max(res, cap);\n    // Move the shorter board inward\n    if (ht[i] < ht[j]) {\n      i++;\n    } else {\n      j--;\n    }\n  }\n  return res;\n}\n
    max_capacity.rs
    /* Max capacity: Greedy algorithm */\nfn max_capacity(ht: &[i32]) -> i32 {\n    // Initialize i, j to be at both ends of the array\n    let mut i = 0;\n    let mut j = ht.len() - 1;\n    // Initial max capacity is 0\n    let mut res = 0;\n    // Loop for greedy selection until the two boards meet\n    while i < j {\n        // Update max capacity\n        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n        res = std::cmp::max(res, cap);\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    res\n}\n
    max_capacity.c
    /* Max capacity: Greedy algorithm */\nint maxCapacity(int ht[], int htLength) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0;\n    int j = htLength - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int capacity = myMin(ht[i], ht[j]) * (j - i);\n        res = myMax(res, capacity);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
    max_capacity.kt
    /* Max capacity: Greedy algorithm */\nfun maxCapacity(ht: IntArray): Int {\n    // Initialize i, j to be at both ends of the array\n    var i = 0\n    var j = ht.size - 1\n    // Initial max capacity is 0\n    var res = 0\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        val cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
    max_capacity.rb
    ### Maximum capacity: greedy ###\ndef max_capacity(ht)\n  # Initialize i, j to be at both ends of the array\n  i, j = 0, ht.length - 1\n  # Initial max capacity is 0\n  res = 0\n\n  # Loop for greedy selection until the two boards meet\n  while i < j\n    # Update max capacity\n    cap = [ht[i], ht[j]].min * (j - i)\n    res = [res, cap].max\n    # Move the shorter board inward\n    if ht[i] < ht[j]\n      i += 1\n    else\n      j -= 1\n    end\n  end\n\n  res\nend\n
    ","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

    The reason greedy is faster than exhaustive enumeration is that each round of greedy selection \"skips\" some states.

    For example, in state \\(cap[i, j]\\), suppose \\(i\\) is the shorter partition and \\(j\\) is the taller partition. If we greedily move the shorter partition \\(i\\) inward by one position, the states shown in Figure 15-12 will be \"skipped.\" This means that their capacities can no longer be checked later.

    \\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\]

    Figure 15-12   States skipped by moving the short partition

    A closer look shows that these skipped states are exactly the states obtained by moving the taller partition \\(j\\) inward. We have already proven that moving the taller partition inward will definitely decrease the capacity. Therefore, none of the skipped states can be the optimal solution, so skipping them does not cause us to miss the optimum.

    The above analysis shows that moving the shorter partition is a \"safe\" operation, and that the greedy strategy is effective.

    ","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   Maximum Product Cutting Problem","text":"

    Question

    Given a positive integer \\(n\\), split it into the sum of at least two positive integers and find the maximum product of the resulting integers, as shown in Figure 15-13.

    Figure 15-13   Problem definition of max product cutting

    Suppose we split \\(n\\) into \\(m\\) integer factors, where the \\(i\\)-th factor is denoted as \\(n_i\\), that is

    \\[ n = \\sum_{i=1}^{m}n_i \\]

    The goal of this problem is to find the maximum product of all integer factors, namely

    \\[ \\max(\\prod_{i=1}^{m}n_i) \\]

    We need to determine how many parts \\(m\\) there should be and what each \\(n_i\\) should be.

    ","path":["Chapter 15. Greedy","15.4   Maximum Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1-determining-the-greedy-strategy","level":3,"title":"1.   Determining the Greedy Strategy","text":"

    As a rule of thumb, the product of two integers is often greater than their sum. Suppose we split off a factor of \\(2\\) from \\(n\\); the resulting product is \\(2(n-2)\\). We compare this product with \\(n\\):

    \\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

    As shown in Figure 15-14, when \\(n \\geq 4\\), splitting out a \\(2\\) will increase the product, which indicates that integers greater than or equal to \\(4\\) should all be split.

    Greedy strategy one: If the splitting scheme contains a factor \\(\\geq 4\\), it should be split further. The final splitting scheme should contain only the factors \\(1\\), \\(2\\), and \\(3\\).

    Figure 15-14   Splitting causes product to increase

    Next, consider which factor is optimal. Among the three factors \\(1\\), \\(2\\), and \\(3\\), clearly \\(1\\) is the worst, because \\(1 \\times (n-1) < n\\) always holds, meaning splitting out \\(1\\) will actually decrease the product.

    As shown in Figure 15-15, when \\(n = 6\\), we have \\(3 \\times 3 > 2 \\times 2 \\times 2\\). This means that splitting out \\(3\\) is better than splitting out \\(2\\).

    Greedy strategy two: In the splitting scheme, there should be at most two \\(2\\)s, because three \\(2\\)s can always be replaced by two \\(3\\)s to obtain a larger product.

    Figure 15-15   Optimal splitting factor

    In summary, the following greedy strategies can be derived.

    1. Input integer \\(n\\), continuously split out factor \\(3\\) until the remainder is \\(0\\), \\(1\\), or \\(2\\).
    2. When the remainder is \\(0\\), it means \\(n\\) is a multiple of \\(3\\), so no further action is needed.
    3. When the remainder is \\(2\\), do not split it further; keep it as is.
    4. When the remainder is \\(1\\), since \\(2 \\times 2 > 1 \\times 3\\), replace the final \\(3\\) and the remaining \\(1\\) with two \\(2\\)s.
    ","path":["Chapter 15. Greedy","15.4   Maximum Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    As shown in Figure 15-16, we do not need loops to split the integer. Instead, we use integer division to obtain the number of \\(3\\)s, denoted by \\(a\\), and the modulo operation to obtain the remainder \\(b\\), giving:

    \\[ n = 3 a + b \\]

    Please note that for the edge case of \\(n \\leq 3\\), a \\(1\\) must be split out, with product \\(1 \\times (n - 1)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py
    def max_product_cutting(n: int) -> int:\n    \"\"\"Max product cutting: Greedy algorithm\"\"\"\n    # When n <= 3, must cut out a 1\n    if n <= 3:\n        return 1 * (n - 1)\n    # Greedily cut out 3, a is the number of 3s, b is the remainder\n    a, b = n // 3, n % 3\n    if b == 1:\n        # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return int(math.pow(3, a - 1)) * 2 * 2\n    if b == 2:\n        # When the remainder is 2, do nothing\n        return int(math.pow(3, a)) * 2\n    # When the remainder is 0, do nothing\n    return int(math.pow(3, a))\n
    max_product_cutting.cpp
    /* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int)pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int)pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int)pow(3, a);\n}\n
    max_product_cutting.java
    /* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int) Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int) Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int) Math.pow(3, a);\n}\n
    max_product_cutting.cs
    /* Max product cutting: Greedy algorithm */\nint MaxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int)Math.Pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int)Math.Pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int)Math.Pow(3, a);\n}\n
    max_product_cutting.go
    /* Max product cutting: Greedy algorithm */\nfunc maxProductCutting(n int) int {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    a := n / 3\n    b := n % 3\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return int(math.Pow(3, float64(a-1))) * 2 * 2\n    }\n    if b == 2 {\n        // When the remainder is 2, do nothing\n        return int(math.Pow(3, float64(a))) * 2\n    }\n    // When the remainder is 0, do nothing\n    return int(math.Pow(3, float64(a)))\n}\n
    max_product_cutting.swift
    /* Max product cutting: Greedy algorithm */\nfunc maxProductCutting(n: Int) -> Int {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = n / 3\n    let b = n % 3\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return pow(3, a - 1) * 2 * 2\n    }\n    if b == 2 {\n        // When the remainder is 2, do nothing\n        return pow(3, a) * 2\n    }\n    // When the remainder is 0, do nothing\n    return pow(3, a)\n}\n
    max_product_cutting.js
    /* Max product cutting: Greedy algorithm */\nfunction maxProductCutting(n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = Math.floor(n / 3);\n    let b = n % 3;\n    if (b === 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // When the remainder is 2, do nothing\n        return Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return Math.pow(3, a);\n}\n
    max_product_cutting.ts
    /* Max product cutting: Greedy algorithm */\nfunction maxProductCutting(n: number): number {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a: number = Math.floor(n / 3);\n    let b: number = n % 3;\n    if (b === 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // When the remainder is 2, do nothing\n        return Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return Math.pow(3, a);\n}\n
    max_product_cutting.dart
    /* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n  // When n <= 3, must cut out a 1\n  if (n <= 3) {\n    return 1 * (n - 1);\n  }\n  // Greedily cut out 3, a is the number of 3s, b is the remainder\n  int a = n ~/ 3;\n  int b = n % 3;\n  if (b == 1) {\n    // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n    return (pow(3, a - 1) * 2 * 2).toInt();\n  }\n  if (b == 2) {\n    // When the remainder is 2, do nothing\n    return (pow(3, a) * 2).toInt();\n  }\n  // When the remainder is 0, do nothing\n  return pow(3, a).toInt();\n}\n
    max_product_cutting.rs
    /* Max product cutting: Greedy algorithm */\nfn max_product_cutting(n: i32) -> i32 {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = n / 3;\n    let b = n % 3;\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        3_i32.pow(a as u32 - 1) * 2 * 2\n    } else if b == 2 {\n        // When the remainder is 2, do nothing\n        3_i32.pow(a as u32) * 2\n    } else {\n        // When the remainder is 0, do nothing\n        3_i32.pow(a as u32)\n    }\n}\n
    max_product_cutting.c
    /* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return pow(3, a);\n}\n
    max_product_cutting.kt
    /* Max product cutting: Greedy algorithm */\nfun maxProductCutting(n: Int): Int {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    val a = n / 3\n    val b = n % 3\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return 3.0.pow((a - 1)).toInt() * 2 * 2\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return 3.0.pow(a).toInt() * 2 * 2\n    }\n    // When the remainder is 0, do nothing\n    return 3.0.pow(a).toInt()\n}\n
    max_product_cutting.rb
    ### Maximum cutting product: greedy ###\ndef max_product_cutting(n)\n  # When n <= 3, must cut out a 1\n  return 1 * (n - 1) if n <= 3\n  # Greedily cut out 3, a is the number of 3s, b is the remainder\n  a, b = n / 3, n % 3\n  # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n  return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n  # When the remainder is 2, do nothing\n  return (3.pow(a) * 2).to_i if b == 2\n  # When the remainder is 0, do nothing\n  3.pow(a).to_i\nend\n

    Figure 15-16   Calculation method for max product cutting

    The time complexity depends on how exponentiation is implemented in the programming language. Taking Python as an example, there are three commonly used ways to compute powers.

    • Both the operator ** and the function pow() have time complexity \\(O(\\log⁡ a)\\).
    • The function math.pow() internally calls the C library's pow() function, which performs floating-point exponentiation, with time complexity \\(O(1)\\).

    Variables \\(a\\) and \\(b\\) use a constant amount of extra space, therefore the space complexity is \\(O(1)\\).

    ","path":["Chapter 15. Greedy","15.4   Maximum Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

    We use proof by contradiction and consider only the case where \\(n \\geq 4\\).

    1. All factors \\(\\leq 3\\): Suppose the optimal splitting scheme includes a factor \\(x \\geq 4\\). Then it can be further split into \\(2(x-2)\\) to obtain a larger (or equal) product. This contradicts the assumption.
    2. The splitting scheme does not contain \\(1\\): Suppose the optimal splitting scheme includes a factor of \\(1\\). Then it can be merged into another factor to obtain a larger product. This contradicts the assumption.
    3. The splitting scheme contains at most two \\(2\\)s: Suppose the optimal splitting scheme includes three \\(2\\)s. Then they can be replaced by two \\(3\\)s, yielding a larger product. This contradicts the assumption.
    ","path":["Chapter 15. Greedy","15.4   Maximum Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   Summary","text":"","path":["Chapter 15. Greedy","15.5   Summary"],"tags":[]},{"location":"chapter_greedy/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Greedy algorithms are typically used to solve optimization problems. The principle is to make locally optimal decisions at each decision stage in hopes of obtaining a globally optimal solution.
    • Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller subproblem in each round, until the problem is solved.
    • Greedy algorithms are not only simple to implement, but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms typically have lower time complexity.
    • In the coin change problem, for certain coin combinations, greedy algorithms can guarantee finding the optimal solution; for other coin combinations, however, greedy algorithms may find very poor solutions.
    • Problems suitable for solving with greedy algorithms have two major properties: greedy choice property and optimal substructure. The greedy choice property represents the effectiveness of the greedy strategy.
    • For some complex problems, proving the greedy choice property is not simple. Relatively speaking, disproving it is easier, such as in the coin change problem.
    • Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the main difficulty.
    • The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting fractions of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction.
    • The max capacity problem can be solved using exhaustive enumeration with time complexity \\(O(n^2)\\). By designing a greedy strategy to move the shorter side inward in each round, the time complexity can be optimized to \\(O(n)\\).
    • In the max product cutting problem, we successively derive two greedy strategies: integers \\(\\geq 4\\) should all continue to be split, and the optimal splitting factor is \\(3\\). The code includes exponentiation operations, and the time complexity depends on the implementation method of exponentiation, typically being \\(O(1)\\) or \\(O(\\log n)\\).
    ","path":["Chapter 15. Greedy","15.5   Summary"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"Chapter 6.   Hashing","text":"

    Abstract

    In the world of computing, a hash table is like a clever librarian.

    It knows how to compute call numbers, allowing it to quickly locate the desired book.

    ","path":["Chapter 6. Hashing","Chapter 6.   Hashing"],"tags":[]},{"location":"chapter_hashing/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 6.1   Hash Table
    • 6.2   Hash Collision
    • 6.3   Hash Algorithm
    • 6.4   Summary
    ","path":["Chapter 6. Hashing","Chapter 6.   Hashing"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   Hash Algorithm","text":"

    The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and separate chaining can only ensure that the hash table functions normally when hash collisions occur, but cannot reduce the frequency of hash collisions.

    If hash collisions occur too frequently, the performance of the hash table will deteriorate drastically. As shown in Figure 6-8, for a separate chaining hash table, in the ideal case, the key-value pairs are evenly distributed across the buckets, achieving optimal query efficiency; in the worst case, all key-value pairs are stored in the same bucket, degrading the time complexity to \\(O(n)\\).

    Figure 6-8   Ideal and worst cases of hash collisions

    The distribution of key-value pairs is determined by the hash function. Recall the steps of the hash function: first compute the hash value, then take it modulo the array length:

    index = hash(key) % capacity\n

    Observing the above formula, when the hash table capacity capacity is fixed, the hash algorithm hash() determines the output value, thereby determining the distribution of key-value pairs in the hash table.

    This means that, to reduce the probability of hash collisions, we should focus on the design of the hash algorithm hash().

    ","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631-goals-of-hash-algorithms","level":2,"title":"6.3.1   Goals of Hash Algorithms","text":"

    To build a hash table that is both fast and robust, a hash algorithm should have the following properties:

    • Determinism: For the same input, the hash algorithm should always produce the same output. Only then can the hash table be reliable.
    • High efficiency: The process of computing the hash value should be fast enough. The smaller the computational overhead, the more practical the hash table.
    • Uniform distribution: The hash algorithm should ensure that key-value pairs are evenly distributed in the hash table. The more uniform the distribution, the lower the probability of hash collisions.

    In fact, hash algorithms are not only used to implement hash tables but are also widely applied in other fields.

    • Password storage: To protect the security of user passwords, systems usually do not store the plaintext passwords but rather the hash values of the passwords. When a user enters a password, the system calculates the hash value of the input and compares it with the stored hash value. If they match, the password is considered correct.
    • Data integrity check: The data sender can calculate the hash value of the data and send it along; the receiver can recalculate the hash value of the received data and compare it with the received hash value. If they match, the data is considered intact.

    For cryptographic applications, hash algorithms need stronger security properties to prevent reverse engineering, such as inferring the original password from a hash value.

    • Unidirectionality: It should be impossible to deduce any information about the input data from the hash value.
    • Collision resistance: It should be extremely difficult to find two different inputs that produce the same hash value.
    • Avalanche effect: Minor changes in the input should lead to significant and unpredictable changes in the output.

    Note that \"uniform distribution\" and \"collision resistance\" are two independent concepts. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input key, the hash function key % 100 can produce a uniformly distributed output. However, this hash algorithm is too simple, and all key with the same last two digits will have the same output, making it easy to deduce a usable key from the hash value, thereby cracking the password.

    ","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632-design-of-hash-algorithms","level":2,"title":"6.3.2   Design of Hash Algorithms","text":"

    The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms.

    • Additive hash: Add up the ASCII codes of each character in the input and use the total sum as the hash value.
    • Multiplicative hash: Leverage the low correlation introduced by multiplication: multiply by a constant at each step and accumulate the ASCII codes of the characters into the hash value.
    • XOR hash: Accumulate the hash value by XORing each element of the input data.
    • Rotating hash: Accumulate the ASCII code of each character into a hash value, performing a rotation operation on the hash value before each accumulation.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py
    def add_hash(key: str) -> int:\n    \"\"\"Additive hash\"\"\"\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    \"\"\"Multiplicative hash\"\"\"\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 hash\"\"\"\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    \"\"\"Rotational hash\"\"\"\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    ### Additive hash ###\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### Multiplicative hash ###\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 hash ###\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### Rotational hash ###\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

    We can observe that the final step of each hash algorithm is to take the result modulo the large prime \\(1000000007\\), ensuring that the hash value stays within a suitable range. This naturally raises a question: why emphasize using a prime modulus, and what are the drawbacks of using a composite modulus?

    In short: using a large prime as the modulus helps maximize the uniformity of hash values. Because a prime shares no common factors with other numbers, it can reduce periodic patterns introduced by the modulo operation and thus mitigate hash collisions.

    For example, suppose we choose the composite number \\(9\\) as the modulus, which can be divided by \\(3\\), then all key divisible by \\(3\\) will be mapped to hash values \\(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} \\]

    If the input key values happen to follow this kind of arithmetic progression, the hash values will cluster, worsening hash collisions. Now suppose we replace modulus with the prime number \\(13\\). Because key and modulus share no common factors, the output hash values become much more evenly distributed.

    \\[ \\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} \\]

    It is worth noting that if the key is guaranteed to be randomly and uniformly distributed, then choosing a prime number or a composite number as the modulus can both produce uniformly distributed hash values. However, when the distribution of key has some periodicity, modulo a composite number is more likely to result in clustering.

    In summary, we usually choose a prime number as the modulus, and this prime number should be large enough to eliminate periodic patterns as much as possible, enhancing the robustness of the hash algorithm.

    ","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633-common-hash-algorithms","level":2,"title":"6.3.3   Common Hash Algorithms","text":"

    It is easy to see that the simple hash algorithms introduced above are fairly \"fragile\" and fall far short of the design goals of hash algorithms. For example, because addition and XOR are commutative, additive hash and XOR hash cannot distinguish strings with the same characters in a different order, which may worsen hash collisions and introduce security risks.

    In practice, we usually use some standard hash algorithms, such as MD5, SHA-1, SHA-2, and SHA-3. They can map input data of any length to a fixed-length hash value.

    Over the past century, hash algorithms have been in a continuous process of upgrading and optimization. Some researchers strive to improve the performance of hash algorithms, while others, including hackers, are dedicated to finding security issues in hash algorithms. Table 6-2 shows hash algorithms commonly used in practical applications.

    • MD5 and SHA-1 have been successfully attacked multiple times and are thus abandoned in various security applications.
    • SHA-2 series, especially SHA-256, is one of the most secure hash algorithms to date, with no successful attacks reported, hence commonly used in various security applications and protocols.
    • SHA-3 has lower implementation costs and higher computational efficiency compared to SHA-2, but its current usage coverage is not as extensive as the SHA-2 series.

    Table 6-2   Common hash algorithms

    MD5 SHA-1 SHA-2 SHA-3 Release Year 1992 1995 2002 2008 Output Length 128 bit 160 bit 256/512 bit 224/256/384/512 bit Hash Collisions Frequent Frequent Rare Rare Security Level Low, has been successfully attacked Low, has been successfully attacked High High Applications Abandoned, still used for data integrity checks Abandoned Cryptocurrency transaction verification, digital signatures, etc. Can be used to replace SHA-2","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#634-hash-values-in-data-structures","level":2,"title":"6.3.4   Hash Values in Data Structures","text":"

    We know that hash table keys can be integers, floating-point numbers, strings, and other data types. Programming languages usually provide built-in hash algorithms for these types to compute bucket indices in a hash table. Taking Python as an example, we can call the hash() function to compute hash values for various data types.

    • The hash values of integers and booleans are their own values.
    • The calculation of hash values for floating-point numbers and strings is more complex, and interested readers are encouraged to study this on their own.
    • The hash value of a tuple is obtained by hashing each of its elements and combining those results into a single hash value.
    • An object's hash value is typically generated from its memory address. By overriding the object's hash method, it can instead be generated from the object's contents.

    Tip

    Be aware that the definition and methods of the built-in hash value calculation functions in different programming languages vary.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py
    num = 3\nhash_num = hash(num)\n# Hash value of integer 3 is 3\n\nbol = True\nhash_bol = hash(bol)\n# Hash value of boolean True is 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# Hash value of decimal 3.14159 is 326484311674566659\n\nstr = \"Hello 算法\"\nhash_str = hash(str)\n# Hash value of string \"Hello 算法\" is 4617003410720528961\n\ntup = (12836, \"小哈\")\nhash_tup = hash(tup)\n# Hash value of tuple (12836, '小哈') is 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# Hash value of ListNode object at 0x1058fd810 is 274267521\n
    built_in_hash.cpp
    int num = 3;\nsize_t hashNum = hash<int>()(num);\n// Hash value of integer 3 is 3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// Hash value of boolean 1 is 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// Hash value of decimal 3.14159 is 4614256650576692846\n\nstring str = \"Hello 算法\";\nsize_t hashStr = hash<string>()(str);\n// Hash value of string \"Hello 算法\" is 15466937326284535026\n\n// In C++, built-in std::hash() only provides hash values for basic data types\n// Hash values for arrays and objects need to be implemented separately\n
    built_in_hash.java
    int num = 3;\nint hashNum = Integer.hashCode(num);\n// Hash value of integer 3 is 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// Hash value of boolean true is 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// Hash value of decimal 3.14159 is -1340954729\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode();\n// Hash value of string \"Hello 算法\" is -727081396\n\nObject[] arr = { 12836, \"小哈\" };\nint hashTup = Arrays.hashCode(arr);\n// Hash value of array [12836, 小哈] is 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// Hash value of ListNode object utils.ListNode@7dc5e7b4 is 2110121908\n
    built_in_hash.cs
    int num = 3;\nint hashNum = num.GetHashCode();\n// Hash value of integer 3 is 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// Hash value of boolean true is 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// Hash value of decimal 3.14159 is -1340954729;\n\nstring str = \"Hello 算法\";\nint hashStr = str.GetHashCode();\n// Hash value of string \"Hello 算法\" is -586107568;\n\nobject[] arr = [12836, \"小哈\"];\nint hashTup = arr.GetHashCode();\n// Hash value of array [12836, 小哈] is 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// Hash value of ListNode object 0 is 39053774;\n
    built_in_hash.go
    // Go does not provide built-in hash code functions\n
    built_in_hash.swift
    let num = 3\nlet hashNum = num.hashValue\n// Hash value of integer 3 is 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// Hash value of boolean true is -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// Hash value of decimal 3.14159 is -2465384235396674631\n\nlet str = \"Hello 算法\"\nlet hashStr = str.hashValue\n// Hash value of string \"Hello 算法\" is -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"小哈\")]\nlet hashTup = arr.hashValue\n// Hash value of array [AnyHashable(12836), AnyHashable(\"小哈\")] is -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// Hash value of ListNode object utils.ListNode is -2434780518035996159\n
    built_in_hash.js
    // JavaScript does not provide built-in hash code functions\n
    built_in_hash.ts
    // TypeScript does not provide built-in hash code functions\n
    built_in_hash.dart
    int num = 3;\nint hashNum = num.hashCode;\n// Hash value of integer 3 is 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// Hash value of boolean true is 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// Hash value of decimal 3.14159 is 2570631074981783\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode;\n// Hash value of string \"Hello 算法\" is 468167534\n\nList arr = [12836, \"小哈\"];\nint hashArr = arr.hashCode;\n// Hash value of array [12836, 小哈] is 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// Hash value of ListNode object Instance of 'ListNode' is 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// Hash value of integer 3 is 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// Hash value of boolean true is 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// Hash value of decimal 3.14159 is 2566941990314602357\n\nlet str = \"Hello 算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// Hash value of string \"Hello 算法\" is 16092673739211250988\n\nlet arr = (&12836, &\"小哈\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// Hash value of tuple (12836, \"小哈\") is 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// Hash value of ListNode object RefCell { value: ListNode { val: 42, next: None } } is 15387811073369036852\n
    built_in_hash.c
    // C does not provide built-in hash code functions\n
    built_in_hash.kt
    val num = 3\nval hashNum = num.hashCode()\n// Hash value of integer 3 is 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// Hash value of boolean true is 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// Hash value of decimal 3.14159 is -1340954729\n\nval str = \"Hello 算法\"\nval hashStr = str.hashCode()\n// Hash value of string \"Hello 算法\" is -727081396\n\nval arr = arrayOf<Any>(12836, \"小哈\")\nval hashTup = arr.hashCode()\n// Hash value of array [12836, 小哈] is 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// Hash value of ListNode object utils.ListNode@1d81eb93 is 495053715\n
    built_in_hash.rb
    num = 3\nhash_num = num.hash\n# Hash value of integer 3 is -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# Hash value of boolean true is -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# Hash value of decimal 3.14159 is -1479186995943067893\n\nstr = \"Hello 算法\"\nhash_str = str.hash\n# Hash value of string \"Hello 算法\" is -4075943250025831763\n\ntup = [12836, '小哈']\nhash_tup = tup.hash\n# Hash value of tuple (12836, '小哈') is 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# Hash value of ListNode object #<ListNode:0x000078133140ab70> is 4302940560806366381\n
    Visualized Execution

    https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

    In many programming languages, only immutable objects can serve as the key in a hash table. If we use a list (dynamic array) as a key, when the contents of the list change, its hash value also changes, and we would no longer be able to find the original value in the hash table.

    Although the member variables of a custom object (such as a linked list node) are mutable, it is hashable. This is because the hash value of an object is usually generated based on its memory address, and even if the contents of the object change, the memory address remains the same, so the hash value remains unchanged.

    You might have noticed that the hash values output in different consoles are different. This is because the Python interpreter adds a random salt to the string hash function each time it starts up. This approach effectively prevents HashDoS attacks and enhances the security of the hash algorithm.

    ","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   Hash Collision","text":"

    The previous section mentioned that, in most cases, the input space of a hash function is much larger than the output space, so theoretically, hash collisions are inevitable. For example, if the input space is all integers and the output space is the array capacity size, then multiple integers will inevitably be mapped to the same bucket index.

    Hash collisions can lead to incorrect query results, severely impacting the usability of the hash table. To address this issue, whenever a hash collision occurs, we can perform hash table expansion until the collision disappears. This approach is simple, straightforward, and effective, but it is very inefficient because hash table expansion involves a large amount of data migration and hash value recalculation. To improve efficiency, we can adopt the following strategies:

    1. Improve the hash table data structure so that the hash table can function normally when hash collisions occur.
    2. Only expand when necessary, that is, only when hash collisions are severe.

    The main approaches to improving a hash table's structure are separate chaining and open addressing.

    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621-separate-chaining","level":2,"title":"6.2.1   Separate Chaining","text":"

    In the original hash table, each bucket can store only one key-value pair. Separate chaining replaces the single element in each bucket with a linked list, treating each key-value pair as a node and storing all colliding key-value pairs in the same list. Figure 6-5 shows an example of a separate chaining hash table.

    Figure 6-5   Separate chaining hash table

    In a hash table implemented with separate chaining, the basic operations work as follows:

    • Querying elements: Input key, compute the bucket index using the hash function, access the head of the corresponding linked list, and traverse the list while comparing keys until the target key-value pair is found.
    • Adding elements: First use the hash function to locate the corresponding linked list, then insert the node (key-value pair) into the list.
    • Deleting elements: Use the hash function to locate the corresponding linked list, then traverse it to find and delete the target node.

    Separate chaining has the following limitations:

    • Increased Space Usage: The linked list contains node pointers, which consume more memory space than arrays.
    • Reduced Query Efficiency: This is because linear traversal of the linked list is required to find the corresponding element.

    The code below provides a simple implementation of a separate chaining hash table, with two things to note:

    • Lists (dynamic arrays) are used instead of linked lists to simplify the code. In this setup, the hash table (array) contains multiple buckets, each of which is a list.
    • This implementation includes a hash table expansion method. When the load factor exceeds \\(\\frac{2}{3}\\), we expand the hash table to \\(2\\) times its original size.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py
    class HashMapChaining:\n    \"\"\"Hash table with separate chaining\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self.size = 0  # Number of key-value pairs\n        self.capacity = 4  # Hash table capacity\n        self.load_thres = 2.0 / 3.0  # Load factor threshold for triggering expansion\n        self.extend_ratio = 2  # Expansion multiplier\n        self.buckets = [[] for _ in range(self.capacity)]  # Bucket array\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Load factor\"\"\"\n        return self.size / self.capacity\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Query operation\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket, if key is found, return corresponding val\n        for pair in bucket:\n            if pair.key == key:\n                return pair.val\n        # If key is not found, return None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Add operation\"\"\"\n        # When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in bucket:\n            if pair.key == key:\n                pair.val = val\n                return\n        # If key does not exist, append key-value pair to the end\n        pair = Pair(key, val)\n        bucket.append(pair)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket and remove key-value pair from it\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        \"\"\"Expand hash table\"\"\"\n        # Temporarily store the original hash table\n        buckets = self.buckets\n        # Initialize expanded new hash table\n        self.capacity *= self.extend_ratio\n        self.buckets = [[] for _ in range(self.capacity)]\n        self.size = 0\n        # Move key-value pairs from original hash table to new hash table\n        for bucket in buckets:\n            for pair in bucket:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Print hash table\"\"\"\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n  private:\n    int size;                       // Number of key-value pairs\n    int capacity;                   // Hash table capacity\n    double loadThres;               // Load factor threshold for triggering expansion\n    int extendRatio;                // Expansion multiplier\n    vector<vector<Pair *>> buckets; // Bucket array\n\n  public:\n    /* Constructor */\n    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n        buckets.resize(capacity);\n    }\n\n    /* Destructor */\n    ~HashMapChaining() {\n        for (auto &bucket : buckets) {\n            for (Pair *pair : bucket) {\n                // Free memory\n                delete pair;\n            }\n        }\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double)size / (double)capacity;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        int index = hashFunc(key);\n        // Traverse bucket, if key is found, return corresponding val\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                return pair->val;\n            }\n        }\n        // Return empty string if key not found\n        return \"\";\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                pair->val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        buckets[index].push_back(new Pair(key, val));\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        auto &bucket = buckets[index];\n        // Traverse bucket and remove key-value pair from it\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); // Remove key-value pair from it\n                delete tmp;                       // Free memory\n                size--;\n                return;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        vector<vector<Pair *>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets.clear();\n        buckets.resize(capacity);\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (auto &bucket : bucketsTmp) {\n            for (Pair *pair : bucket) {\n                put(pair->key, pair->val);\n                // Free memory\n                delete pair;\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    int size; // Number of key-value pairs\n    int capacity; // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio; // Expansion multiplier\n    List<List<Pair>> buckets; // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Query operation */\n    String get(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket, if key is found, return corresponding val\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    void put(int key, String val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        Pair pair = new Pair(key, val);\n        bucket.add(pair);\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket and remove key-value pair from it\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        List<List<Pair>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\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        // Move key-value pairs from original hash table to new hash table\n        for (List<Pair> bucket : bucketsTmp) {\n            for (Pair pair : bucket) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    int size; // Number of key-value pairs\n    int capacity; // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio; // Expansion multiplier\n    List<List<Pair>> buckets; // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        // Traverse bucket, if key is found, return corresponding val\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        int index = HashFunc(key);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        buckets[index].Add(new Pair(key, val));\n        size++;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Traverse bucket and remove key-value pair from it\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    /* Expand hash table */\n    void Extend() {\n        // Temporarily store the original hash table\n        List<List<Pair>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\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        // Move key-value pairs from original hash table to new hash table\n        foreach (List<Pair> bucket in bucketsTmp) {\n            foreach (Pair pair in bucket) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\ntype hashMapChaining struct {\n    size        int      // Number of key-value pairs\n    capacity    int      // Hash table capacity\n    loadThres   float64  // Load factor threshold for triggering expansion\n    extendRatio int      // Expansion multiplier\n    buckets     [][]pair // Bucket array\n}\n\n/* Constructor */\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/* Hash function */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n    return key % m.capacity\n}\n\n/* Load factor */\nfunc (m *hashMapChaining) loadFactor() float64 {\n    return float64(m.size) / float64(m.capacity)\n}\n\n/* Query operation */\nfunc (m *hashMapChaining) get(key int) string {\n    idx := m.hashFunc(key)\n    bucket := m.buckets[idx]\n    // Traverse bucket, if key is found, return corresponding val\n    for _, p := range bucket {\n        if p.key == key {\n            return p.val\n        }\n    }\n    // Return empty string if key not found\n    return \"\"\n}\n\n/* Add operation */\nfunc (m *hashMapChaining) put(key int, val string) {\n    // When load factor exceeds threshold, perform expansion\n    if m.loadFactor() > m.loadThres {\n        m.extend()\n    }\n    idx := m.hashFunc(key)\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\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    // If key does not exist, append key-value pair to the end\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/* Remove operation */\nfunc (m *hashMapChaining) remove(key int) {\n    idx := m.hashFunc(key)\n    // Traverse bucket and remove key-value pair from it\n    for i, p := range m.buckets[idx] {\n        if p.key == key {\n            // Slice deletion\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/* Expand hash table */\nfunc (m *hashMapChaining) extend() {\n    // Temporarily store the original hash table\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    // Initialize expanded new hash table\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    // Move key-value pairs from original hash table to new hash table\n    for _, bucket := range tmpBuckets {\n        for _, p := range bucket {\n            m.put(p.key, p.val)\n        }\n    }\n}\n\n/* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    var loadThres: Double // Load factor threshold for triggering expansion\n    var extendRatio: Int // Expansion multiplier\n    var buckets: [[Pair]] // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Load factor */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket, if key is found, return corresponding val\n        for pair in bucket {\n            if pair.key == key {\n                return pair.val\n            }\n        }\n        // Return nil if key not found\n        return nil\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if loadFactor() > loadThres {\n            extend()\n        }\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in bucket {\n            if pair.key == key {\n                pair.val = val\n                return\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        let pair = Pair(key: key, val: val)\n        buckets[index].append(pair)\n        size += 1\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket and remove key-value pair from it\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    /* Expand hash table */\n    func extend() {\n        // Temporarily store the original hash table\n        let bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = Array(repeating: [], count: capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for bucket in bucketsTmp {\n            for pair in bucket {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    #size; // Number of key-value pairs\n    #capacity; // Hash table capacity\n    #loadThres; // Load factor threshold for triggering expansion\n    #extendRatio; // Expansion multiplier\n    #buckets; // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Query operation */\n    get(key) {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if key is found, return corresponding val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key, val) {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key) {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Traverse bucket and remove key-value pair from it\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    /* Expand hash table */\n    #extend() {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    #size: number; // Number of key-value pairs\n    #capacity: number; // Hash table capacity\n    #loadThres: number; // Load factor threshold for triggering expansion\n    #extendRatio: number; // Expansion multiplier\n    #buckets: Pair[][]; // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    #hashFunc(key: number): number {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor(): number {\n        return this.#size / this.#capacity;\n    }\n\n    /* Query operation */\n    get(key: number): string | null {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if key is found, return corresponding val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key: number, val: string): void {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key: number): void {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Traverse bucket and remove key-value pair from it\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    /* Expand hash table */\n    #extend(): void {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n  late int size; // Number of key-value pairs\n  late int capacity; // Hash table capacity\n  late double loadThres; // Load factor threshold for triggering expansion\n  late int extendRatio; // Expansion multiplier\n  late List<List<Pair>> buckets; // Bucket array\n\n  /* Constructor */\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  /* Hash function */\n  int hashFunc(int key) {\n    return key % capacity;\n  }\n\n  /* Load factor */\n  double loadFactor() {\n    return size / capacity;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket, if key is found, return corresponding val\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        return pair.val;\n      }\n    }\n    // If key is not found, return null\n    return null;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor() > loadThres) {\n      extend();\n    }\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        pair.val = val;\n        return;\n      }\n    }\n    // If key does not exist, append key-value pair to the end\n    Pair pair = Pair(key, val);\n    bucket.add(pair);\n    size++;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket and remove key-value pair from it\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        bucket.remove(pair);\n        size--;\n        break;\n      }\n    }\n  }\n\n  /* Expand hash table */\n  void extend() {\n    // Temporarily store the original hash table\n    List<List<Pair>> bucketsTmp = buckets;\n    // Initialize expanded new hash table\n    capacity *= extendRatio;\n    buckets = List.generate(capacity, (_) => []);\n    size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (List<Pair> bucket in bucketsTmp) {\n      for (Pair pair in bucket) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Print hash table */\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
    /* Hash table with separate chaining */\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    /* Constructor */\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    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % self.capacity\n    }\n\n    /* Load factor */\n    fn load_factor(&self) -> f32 {\n        self.size as f32 / self.capacity as f32\n    }\n\n    /* Remove operation */\n    fn remove(&mut self, key: i32) -> Option<String> {\n        let index = self.hash_func(key);\n\n        // Traverse bucket and remove key-value pair from it\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        // If key is not found, return None\n        None\n    }\n\n    /* Expand hash table */\n    fn extend(&mut self) {\n        // Temporarily store the original hash table\n        let buckets_tmp = std::mem::take(&mut self.buckets);\n\n        // Initialize expanded new hash table\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![Vec::new(); self.capacity as usize];\n        self.size = 0;\n\n        // Move key-value pairs from original hash table to new hash table\n        for bucket in buckets_tmp {\n            for pair in bucket {\n                self.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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    /* Add operation */\n    fn put(&mut self, key: i32, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n\n        let index = self.hash_func(key);\n\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in self.buckets[index].iter_mut() {\n            if pair.key == key {\n                pair.val = val;\n                return;\n            }\n        }\n\n        // If key does not exist, append key-value pair to the end\n        let pair = Pair { key, val };\n        self.buckets[index].push(pair);\n        self.size += 1;\n    }\n\n    /* Query operation */\n    fn get(&self, key: i32) -> Option<&str> {\n        let index = self.hash_func(key);\n\n        // Traverse bucket, if key is found, return corresponding val\n        for pair in self.buckets[index].iter() {\n            if pair.key == key {\n                return Some(&pair.val);\n            }\n        }\n\n        // If key is not found, return None\n        None\n    }\n}\n
    hash_map_chaining.c
    /* Linked list node */\ntypedef struct Node {\n    Pair *pair;\n    struct Node *next;\n} Node;\n\n/* Hash table with separate chaining */\ntypedef struct {\n    int size;         // Number of key-value pairs\n    int capacity;     // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio;  // Expansion multiplier\n    Node **buckets;   // Bucket array\n} HashMapChaining;\n\n/* Constructor */\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/* Destructor */\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/* Hash function */\nint hashFunc(HashMapChaining *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Load factor */\ndouble loadFactor(HashMapChaining *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Query operation */\nchar *get(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    // Traverse bucket, if key is found, return corresponding 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 \"\"; // Return empty string if key not found\n}\n\n/* Add operation */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    int index = hashFunc(hashMap, key);\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            strcpy(cur->pair->val, val); // If specified key is found, update corresponding val and return\n            return;\n        }\n        cur = cur->next;\n    }\n    // If key not found, add key-value pair to list head\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/* Expand hash table */\nvoid extend(HashMapChaining *hashMap) {\n    // Temporarily store the original hash table\n    int oldCapacity = hashMap->capacity;\n    Node **oldBuckets = hashMap->buckets;\n    // Initialize expanded new hash table\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    // Move key-value pairs from original hash table to new hash table\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            // Free memory\n            free(temp->pair);\n            free(temp);\n        }\n    }\n\n    free(oldBuckets);\n}\n\n/* Remove operation */\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            // Remove key-value pair from it\n            if (pre) {\n                pre->next = cur->next;\n            } else {\n                hashMap->buckets[index] = cur->next;\n            }\n            // Free memory\n            free(cur->pair);\n            free(cur);\n            hashMap->size--;\n            return;\n        }\n        pre = cur;\n        cur = cur->next;\n    }\n}\n\n/* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    val loadThres: Double // Load factor threshold for triggering expansion\n    val extendRatio: Int // Expansion multiplier\n    var buckets: MutableList<MutableList<Pair>> // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Load factor */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket, if key is found, return corresponding val\n        for (pair in bucket) {\n            if (pair.key == key) return pair._val\n        }\n        // If key is not found, return null\n        return null\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (pair in bucket) {\n            if (pair.key == key) {\n                pair._val = _val\n                return\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        val pair = Pair(key, _val)\n        bucket.add(pair)\n        size++\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket and remove key-value pair from it\n        for (pair in bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair)\n                size--\n                break\n            }\n        }\n    }\n\n    /* Expand hash table */\n    fun extend() {\n        // Temporarily store the original hash table\n        val bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        // mutablelist has no fixed size\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for (bucket in bucketsTmp) {\n            for (pair in bucket) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Print hash table */\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
    ### Hash map with chaining ###\nclass HashMapChaining\n  ### Constructor ###\n  def initialize\n    @size = 0 # Number of key-value pairs\n    @capacity = 4 # Hash table capacity\n    @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion\n    @extend_ratio = 2 # Expansion multiplier\n    @buckets = Array.new(@capacity) { [] } # Bucket array\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Load factor ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Query operation ###\n  def get(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket, if key is found, return corresponding val\n    for pair in bucket\n      return pair.val if pair.key == key\n    end\n    # Return nil if key not found\n    nil\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    # When load factor exceeds threshold, perform expansion\n    extend if load_factor > @load_thres\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket, if specified key is encountered, update corresponding val and return\n    for pair in bucket\n      if pair.key == key\n        pair.val = val\n        return\n      end\n    end\n    # If key does not exist, append key-value pair to the end\n    pair = Pair.new(key, val)\n    bucket << pair\n    @size += 1\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket and remove key-value pair from it\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  ### Expand hash table ###\n  def extend\n    # Temporarily store original hash table\n    buckets = @buckets\n    # Initialize expanded new hash table\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity) { [] }\n    @size = 0\n    # Move key-value pairs from original hash table to new hash table\n    for bucket in buckets\n      for pair in bucket\n        put(pair.key, pair.val)\n      end\n    end\n  end\n\n  ### Print hash table ###\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

    It's worth noting that when the linked list becomes very long, the query time \\(O(n)\\) is poor. In this case, the linked list can be converted into an AVL tree or a red-black tree, reducing the time complexity of lookups to \\(O(\\log n)\\).

    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622-open-addressing","level":2,"title":"6.2.2   Open Addressing","text":"

    Open addressing does not introduce additional data structures. Instead, it handles hash collisions through repeated probing. Common probing strategies include linear probing, quadratic probing, and multiple hashing.

    Let's use linear probing as an example to introduce the mechanism of open addressing hash tables.

    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1-linear-probing","level":3,"title":"1.   Linear Probing","text":"

    Linear probing uses a fixed step size to probe sequentially, so its operations differ somewhat from those of an ordinary hash table.

    • Inserting elements: Compute the bucket index using the hash function. If the bucket is already occupied, continue probing forward from the collision position with a fixed step size (usually \\(1\\)) until an empty bucket is found, then insert the element there.
    • Searching for elements: If a collision occurs, continue probing forward with the same step size until the corresponding element is found and return its value; if an empty bucket is encountered, the target element is not in the hash table, so return None.

    Figure 6-6 shows the distribution of key-value pairs in an open-addressing hash table that uses linear probing. Under this hash function, keys with the same last two digits are mapped to the same bucket. Linear probing then places them in that bucket and the subsequent buckets.

    Figure 6-6   Distribution of key-value pairs in open addressing (linear probing) hash table

    However, linear probing is prone to clustering. Specifically, the longer a contiguous occupied region in the array becomes, the more likely new collisions are to occur within that region. This in turn makes the cluster grow even further, creating a vicious cycle that gradually degrades the efficiency of insertion, deletion, lookup, and update operations.

    It's important to note that we cannot directly delete elements from an open-addressing hash table. Deleting an element creates an empty bucket None in the array. During lookup, once linear probing reaches that empty bucket, it stops, which means any elements stored farther along the probe sequence become unreachable. As a result, the program may incorrectly conclude that those elements do not exist, as shown in Figure 6-7.

    Figure 6-7   Query issues caused by deletion in open addressing

    To solve this problem, we can adopt lazy deletion: instead of directly removing an element from the hash table, use a constant TOMBSTONE to mark the bucket. Under this mechanism, both None and TOMBSTONE denote buckets that can accept key-value pairs. The difference is that when linear probing encounters TOMBSTONE, it must continue probing, because key-value pairs may still exist farther along the sequence.

    However, lazy deletion may accelerate hash-table performance degradation. Each deletion leaves behind a marker, and as the number of TOMBSTONE entries grows, search time increases as well, because linear probing may need to skip over multiple tombstones before finding the target element.

    To address this, we can record the index of the first TOMBSTONE encountered during linear probing and swap the found target element into that position. The benefit is that each query or insertion can move elements closer to their ideal positions, that is, closer to where probing begins, which improves lookup efficiency.

    The code below implements an open addressing (linear probing) hash table with lazy deletion. To make better use of the hash table space, we treat the hash table as a \"circular array\". When going beyond the end of the array, we return to the beginning and continue traversing.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py
    class HashMapOpenAddressing:\n    \"\"\"Hash table with open addressing\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self.size = 0  # Number of key-value pairs\n        self.capacity = 4  # Hash table capacity\n        self.load_thres = 2.0 / 3.0  # Load factor threshold for triggering expansion\n        self.extend_ratio = 2  # Expansion multiplier\n        self.buckets: list[Pair | None] = [None] * self.capacity  # Bucket array\n        self.TOMBSTONE = Pair(-1, \"-1\")  # Removal marker\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Load factor\"\"\"\n        return self.size / self.capacity\n\n    def find_bucket(self, key: int) -> int:\n        \"\"\"Search for bucket index corresponding to key\"\"\"\n        index = self.hash_func(key)\n        first_tombstone = -1\n        # Linear probing, break when encountering an empty bucket\n        while self.buckets[index] is not None:\n            # If key is encountered, return the corresponding bucket index\n            if self.buckets[index].key == key:\n                # If a removal marker was encountered before, move the key-value pair to that index\n                if first_tombstone != -1:\n                    self.buckets[first_tombstone] = self.buckets[index]\n                    self.buckets[index] = self.TOMBSTONE\n                    return first_tombstone  # Return the moved bucket index\n                return index  # Return bucket index\n            # Record the first removal marker encountered\n            if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n                first_tombstone = index\n            # Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % self.capacity\n        # If key does not exist, return the index for insertion\n        return index if first_tombstone == -1 else first_tombstone\n\n    def get(self, key: int) -> str:\n        \"\"\"Query operation\"\"\"\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, return corresponding val\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            return self.buckets[index].val\n        # If key-value pair does not exist, return None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Add operation\"\"\"\n        # When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, overwrite val and return\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index].val = val\n            return\n        # If key-value pair does not exist, add the key-value pair\n        self.buckets[index] = Pair(key, val)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, overwrite it with removal marker\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        \"\"\"Expand hash table\"\"\"\n        # Temporarily store the original hash table\n        buckets_tmp = self.buckets\n        # Initialize expanded new hash table\n        self.capacity *= self.extend_ratio\n        self.buckets = [None] * self.capacity\n        self.size = 0\n        # Move key-value pairs from original hash table to new hash table\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        \"\"\"Print hash table\"\"\"\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n  private:\n    int size;                             // Number of key-value pairs\n    int capacity = 4;                     // Hash table capacity\n    const double loadThres = 2.0 / 3.0;     // Load factor threshold for triggering expansion\n    const int extendRatio = 2;            // Expansion multiplier\n    vector<Pair *> buckets;               // Bucket array\n    Pair *TOMBSTONE = new Pair(-1, \"-1\"); // Removal marker\n\n  public:\n    /* Constructor */\n    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n    }\n\n    /* Destructor */\n    ~HashMapOpenAddressing() {\n        for (Pair *pair : buckets) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                delete pair;\n            }\n        }\n        delete TOMBSTONE;\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != nullptr) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index]->key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            return buckets[index]->val;\n        }\n        // Return empty string if key-value pair does not exist\n        return \"\";\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            buckets[index]->val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            delete buckets[index];\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        vector<Pair *> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = vector<Pair *>(capacity, nullptr);\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\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    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private int size; // Number of key-value pairs\n    private int capacity = 4; // Hash table capacity\n    private final double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n    private final int extendRatio = 2; // Expansion multiplier\n    private Pair[] buckets; // Bucket array\n    private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // Removal marker\n\n    /* Constructor */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Hash function */\n    private int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    private double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    private int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index].key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    public String get(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void put(int key, String val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    public void remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    private void extend() {\n        // Temporarily store the original hash table\n        Pair[] bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (Pair pair : bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    int size; // Number of key-value pairs\n    int capacity = 4; // Hash table capacity\n    double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n    int extendRatio = 2; // Expansion multiplier\n    Pair[] buckets; // Bucket array\n    Pair TOMBSTONE = new(-1, \"-1\"); // Removal marker\n\n    /* Constructor */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    int FindBucket(int key) {\n        int index = HashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index].key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    void Extend() {\n        // Temporarily store the original hash table\n        Pair[] bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        foreach (Pair pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with open addressing */\ntype hashMapOpenAddressing struct {\n    size        int     // Number of key-value pairs\n    capacity    int     // Hash table capacity\n    loadThres   float64 // Load factor threshold for triggering expansion\n    extendRatio int     // Expansion multiplier\n    buckets     []*pair // Bucket array\n    TOMBSTONE   *pair   // Removal marker\n}\n\n/* Constructor */\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/* Hash function */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n    return key % h.capacity // Calculate hash value based on key\n}\n\n/* Load factor */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n    return float64(h.size) / float64(h.capacity) // Calculate current load factor\n}\n\n/* Search for bucket index corresponding to key */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n    index := h.hashFunc(key) // Get initial index\n    firstTombstone := -1     // Record position of first TOMBSTONE encountered\n    for h.buckets[index] != nil {\n        if h.buckets[index].key == key {\n            if firstTombstone != -1 {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                h.buckets[firstTombstone] = h.buckets[index]\n                h.buckets[index] = h.TOMBSTONE\n                return firstTombstone // Return the moved bucket index\n            }\n            return index // Return found index\n        }\n        if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE {\n            firstTombstone = index // Record position of first deletion marker encountered\n        }\n        index = (index + 1) % h.capacity // Linear probing, wrap around to head if past tail\n    }\n    // If key does not exist, return the index for insertion\n    if firstTombstone != -1 {\n        return firstTombstone\n    }\n    return index\n}\n\n/* Query operation */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        return h.buckets[index].val // If key-value pair is found, return corresponding val\n    }\n    return \"\" // Return \"\" if key-value pair does not exist\n}\n\n/* Add operation */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n    if h.loadFactor() > h.loadThres {\n        h.extend() // When load factor exceeds threshold, perform expansion\n    }\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n        h.buckets[index] = &pair{key, val} // If key-value pair does not exist, add the key-value pair\n        h.size++\n    } else {\n        h.buckets[index].val = val // If key-value pair found, overwrite val\n    }\n}\n\n/* Remove operation */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        h.buckets[index] = h.TOMBSTONE // If key-value pair is found, overwrite it with removal marker\n        h.size--\n    }\n}\n\n/* Expand hash table */\nfunc (h *hashMapOpenAddressing) extend() {\n    oldBuckets := h.buckets               // Temporarily store the original hash table\n    h.capacity *= h.extendRatio           // Update capacity\n    h.buckets = make([]*pair, h.capacity) // Initialize expanded new hash table\n    h.size = 0                            // Reset size\n    // Move key-value pairs from original hash table to new hash table\n    for _, pair := range oldBuckets {\n        if pair != nil && pair != h.TOMBSTONE {\n            h.put(pair.key, pair.val)\n        }\n    }\n}\n\n/* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    var loadThres: Double // Load factor threshold for triggering expansion\n    var extendRatio: Int // Expansion multiplier\n    var buckets: [Pair?] // Bucket array\n    var TOMBSTONE: Pair // Removal marker\n\n    /* Constructor */\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    /* Hash function */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Load factor */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Search for bucket index corresponding to key */\n    func findBucket(key: Int) -> Int {\n        var index = hashFunc(key: key)\n        var firstTombstone = -1\n        // Linear probing, break when encountering an empty bucket\n        while buckets[index] != nil {\n            // If key is encountered, return the corresponding bucket index\n            if buckets[index]!.key == key {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if firstTombstone != -1 {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Return the moved bucket index\n                }\n                return index // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if firstTombstone == -1 && buckets[index] == TOMBSTONE {\n                firstTombstone = index\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, return corresponding val\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            return buckets[index]!.val\n        }\n        // If key-value pair does not exist, return null\n        return nil\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if loadFactor() > loadThres {\n            extend()\n        }\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, overwrite val and return\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index]!.val = val\n            return\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = Pair(key: key, val: val)\n        size += 1\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, overwrite it with removal marker\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index] = TOMBSTONE\n            size -= 1\n        }\n    }\n\n    /* Expand hash table */\n    func extend() {\n        // Temporarily store the original hash table\n        let bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = Array(repeating: nil, count: capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for pair in bucketsTmp {\n            if let pair, pair != TOMBSTONE {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    #size; // Number of key-value pairs\n    #capacity; // Hash table capacity\n    #loadThres; // Load factor threshold for triggering expansion\n    #extendRatio; // Expansion multiplier\n    #buckets; // Bucket array\n    #TOMBSTONE; // Removal marker\n\n    /* Constructor */\n    constructor() {\n        this.#size = 0; // Number of key-value pairs\n        this.#capacity = 4; // Hash table capacity\n        this.#loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n        this.#extendRatio = 2; // Expansion multiplier\n        this.#buckets = Array(this.#capacity).fill(null); // Bucket array\n        this.#TOMBSTONE = new Pair(-1, '-1'); // Removal marker\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    #findBucket(key) {\n        let index = this.#hashFunc(key);\n        let firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (this.#buckets[index] !== null) {\n            // If key is encountered, return the corresponding bucket index\n            if (this.#buckets[index].key === key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone !== -1) {\n                    this.#buckets[firstTombstone] = this.#buckets[index];\n                    this.#buckets[index] = this.#TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (\n                firstTombstone === -1 &&\n                this.#buckets[index] === this.#TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % this.#capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    get(key) {\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            return this.#buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key, val) {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        this.#buckets[index] = new Pair(key, val);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key) {\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\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    /* Expand hash table */\n    #extend() {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = Array(this.#capacity).fill(null);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\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    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private size: number; // Number of key-value pairs\n    private capacity: number; // Hash table capacity\n    private loadThres: number; // Load factor threshold for triggering expansion\n    private extendRatio: number; // Expansion multiplier\n    private buckets: Array<Pair | null>; // Bucket array\n    private TOMBSTONE: Pair; // Removal marker\n\n    /* Constructor */\n    constructor() {\n        this.size = 0; // Number of key-value pairs\n        this.capacity = 4; // Hash table capacity\n        this.loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n        this.extendRatio = 2; // Expansion multiplier\n        this.buckets = Array(this.capacity).fill(null); // Bucket array\n        this.TOMBSTONE = new Pair(-1, '-1'); // Removal marker\n    }\n\n    /* Hash function */\n    private hashFunc(key: number): number {\n        return key % this.capacity;\n    }\n\n    /* Load factor */\n    private loadFactor(): number {\n        return this.size / this.capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    private findBucket(key: number): number {\n        let index = this.hashFunc(key);\n        let firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (this.buckets[index] !== null) {\n            // If key is encountered, return the corresponding bucket index\n            if (this.buckets[index]!.key === key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone !== -1) {\n                    this.buckets[firstTombstone] = this.buckets[index];\n                    this.buckets[index] = this.TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (\n                firstTombstone === -1 &&\n                this.buckets[index] === this.TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % this.capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    get(key: number): string | null {\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            return this.buckets[index]!.val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key: number, val: string): void {\n        // When load factor exceeds threshold, perform expansion\n        if (this.loadFactor() > this.loadThres) {\n            this.extend();\n        }\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index]!.val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        this.buckets[index] = new Pair(key, val);\n        this.size++;\n    }\n\n    /* Remove operation */\n    remove(key: number): void {\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\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    /* Expand hash table */\n    private extend(): void {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.buckets;\n        // Initialize expanded new hash table\n        this.capacity *= this.extendRatio;\n        this.buckets = Array(this.capacity).fill(null);\n        this.size = 0;\n        // Move key-value pairs from original hash table to new hash table\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    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n  late int _size; // Number of key-value pairs\n  int _capacity = 4; // Hash table capacity\n  double _loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n  int _extendRatio = 2; // Expansion multiplier\n  late List<Pair?> _buckets; // Bucket array\n  Pair _TOMBSTONE = Pair(-1, \"-1\"); // Removal marker\n\n  /* Constructor */\n  HashMapOpenAddressing() {\n    _size = 0;\n    _buckets = List.generate(_capacity, (index) => null);\n  }\n\n  /* Hash function */\n  int hashFunc(int key) {\n    return key % _capacity;\n  }\n\n  /* Load factor */\n  double loadFactor() {\n    return _size / _capacity;\n  }\n\n  /* Search for bucket index corresponding to key */\n  int findBucket(int key) {\n    int index = hashFunc(key);\n    int firstTombstone = -1;\n    // Linear probing, break when encountering an empty bucket\n    while (_buckets[index] != null) {\n      // If key is encountered, return the corresponding bucket index\n      if (_buckets[index]!.key == key) {\n        // If a removal marker was encountered before, move the key-value pair to that index\n        if (firstTombstone != -1) {\n          _buckets[firstTombstone] = _buckets[index];\n          _buckets[index] = _TOMBSTONE;\n          return firstTombstone; // Return the moved bucket index\n        }\n        return index; // Return bucket index\n      }\n      // Record the first removal marker encountered\n      if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) {\n        firstTombstone = index;\n      }\n      // Calculate bucket index, wrap around to the head if past the tail\n      index = (index + 1) % _capacity;\n    }\n    // If key does not exist, return the index for insertion\n    return firstTombstone == -1 ? index : firstTombstone;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, return corresponding val\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      return _buckets[index]!.val;\n    }\n    // If key-value pair does not exist, return null\n    return null;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor() > _loadThres) {\n      extend();\n    }\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, overwrite val and return\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index]!.val = val;\n      return;\n    }\n    // If key-value pair does not exist, add the key-value pair\n    _buckets[index] = new Pair(key, val);\n    _size++;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, overwrite it with removal marker\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index] = _TOMBSTONE;\n      _size--;\n    }\n  }\n\n  /* Expand hash table */\n  void extend() {\n    // Temporarily store the original hash table\n    List<Pair?> bucketsTmp = _buckets;\n    // Initialize expanded new hash table\n    _capacity *= _extendRatio;\n    _buckets = List.generate(_capacity, (index) => null);\n    _size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (Pair? pair in bucketsTmp) {\n      if (pair != null && pair != _TOMBSTONE) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Print hash table */\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
    /* Hash table with open addressing */\nstruct HashMapOpenAddressing {\n    size: usize,                // Number of key-value pairs\n    capacity: usize,            // Hash table capacity\n    load_thres: f64,            // Load factor threshold for triggering expansion\n    extend_ratio: usize,        // Expansion multiplier\n    buckets: Vec<Option<Pair>>, // Bucket array\n    TOMBSTONE: Option<Pair>,    // Removal marker\n}\n\nimpl HashMapOpenAddressing {\n    /* Constructor */\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    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        (key % self.capacity as i32) as usize\n    }\n\n    /* Load factor */\n    fn load_factor(&self) -> f64 {\n        self.size as f64 / self.capacity as f64\n    }\n\n    /* Search for bucket index corresponding to 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        // Linear probing, break when encountering an empty bucket\n        while self.buckets[index].is_some() {\n            // If key is found, return corresponding bucket index\n            if self.buckets[index].as_ref().unwrap().key == key {\n                // If deletion marker was encountered before, move key-value pair to that index\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; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE {\n                first_tombstone = index as i32;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % self.capacity;\n        }\n        // If key does not exist, return the index for insertion\n        if first_tombstone == -1 {\n            index\n        } else {\n            first_tombstone as usize\n        }\n    }\n\n    /* Query operation */\n    fn get(&mut self, key: i32) -> Option<&str> {\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, return corresponding 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        // If key-value pair does not exist, return null\n        None\n    }\n\n    /* Add operation */\n    fn put(&mut self, key: i32, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, overwrite val and return\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        // If key-value pair does not exist, add the key-value pair\n        self.buckets[index] = Some(Pair { key, val });\n        self.size += 1;\n    }\n\n    /* Remove operation */\n    fn remove(&mut self, key: i32) {\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, overwrite it with removal marker\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    /* Expand hash table */\n    fn extend(&mut self) {\n        // Temporarily store the original hash table\n        let buckets_tmp = self.buckets.clone();\n        // Initialize expanded new hash table\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![None; self.capacity];\n        self.size = 0;\n\n        // Move key-value pairs from original hash table to new hash table\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    /* Print hash table */\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
    /* Hash table with open addressing */\ntypedef struct {\n    int size;         // Number of key-value pairs\n    int capacity;     // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio;  // Expansion multiplier\n    Pair **buckets;   // Bucket array\n    Pair *TOMBSTONE;  // Removal marker\n} HashMapOpenAddressing;\n\n/* Constructor */\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/* Destructor */\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/* Hash function */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Load factor */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Search for bucket index corresponding to key */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    int firstTombstone = -1;\n    // Linear probing, break when encountering an empty bucket\n    while (hashMap->buckets[index] != NULL) {\n        // If key is encountered, return the corresponding bucket index\n        if (hashMap->buckets[index]->key == key) {\n            // If a removal marker was encountered before, move the key-value pair to that index\n            if (firstTombstone != -1) {\n                hashMap->buckets[firstTombstone] = hashMap->buckets[index];\n                hashMap->buckets[index] = hashMap->TOMBSTONE;\n                return firstTombstone; // Return the moved bucket index\n            }\n            return index; // Return bucket index\n        }\n        // Record the first removal marker encountered\n        if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {\n            firstTombstone = index;\n        }\n        // Calculate bucket index, wrap around to the head if past the tail\n        index = (index + 1) % hashMap->capacity;\n    }\n    // If key does not exist, return the index for insertion\n    return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* Query operation */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, return corresponding val\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        return hashMap->buckets[index]->val;\n    }\n    // Return empty string if key-value pair does not exist\n    return \"\";\n}\n\n/* Add operation */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, overwrite val and return\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    // If key-value pair does not exist, add the key-value pair\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/* Remove operation */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, overwrite it with removal marker\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/* Expand hash table */\nvoid extend(HashMapOpenAddressing *hashMap) {\n    // Temporarily store the original hash table\n    Pair **bucketsTmp = hashMap->buckets;\n    int oldCapacity = hashMap->capacity;\n    // Initialize expanded new hash table\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->size = 0;\n    // Move key-value pairs from original hash table to new hash table\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/* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private var size: Int               // Number of key-value pairs\n    private var capacity: Int           // Hash table capacity\n    private val loadThres: Double       // Load factor threshold for triggering expansion\n    private val extendRatio: Int        // Expansion multiplier\n    private var buckets: Array<Pair?>   // Bucket array\n    private val TOMBSTONE: Pair         // Removal marker\n\n    /* Constructor */\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    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Load factor */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Search for bucket index corresponding to key */\n    fun findBucket(key: Int): Int {\n        var index = hashFunc(key)\n        var firstTombstone = -1\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index]?.key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Return the moved bucket index\n                }\n                return index // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity\n        }\n        // If key does not exist, return the index for insertion\n        return if (firstTombstone == -1) index else firstTombstone\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index]?._val\n        }\n        // If key-value pair does not exist, return null\n        return null\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index]!!._val = _val\n            return\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = Pair(key, _val)\n        size++\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE\n            size--\n        }\n    }\n\n    /* Expand hash table */\n    fun extend() {\n        // Temporarily store the original hash table\n        val bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = arrayOfNulls(capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for (pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Print hash table */\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
    ### Hash map with open addressing ###\nclass HashMapOpenAddressing\n  TOMBSTONE = Pair.new(-1, '-1') # Removal marker\n\n  ### Constructor ###\n  def initialize\n    @size = 0 # Number of key-value pairs\n    @capacity = 4 # Hash table capacity\n    @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion\n    @extend_ratio = 2 # Expansion multiplier\n    @buckets = Array.new(@capacity) # Bucket array\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Load factor ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Search bucket index for key ###\n  def find_bucket(key)\n    index = hash_func(key)\n    first_tombstone = -1\n    # Linear probing, break when encountering an empty bucket\n    while !@buckets[index].nil?\n      # If key is encountered, return the corresponding bucket index\n      if @buckets[index].key == key\n        # If a removal marker was encountered before, move the key-value pair to that index\n        if first_tombstone != -1\n          @buckets[first_tombstone] = @buckets[index]\n          @buckets[index] = TOMBSTONE\n          return first_tombstone # Return the moved bucket index\n        end\n        return index # Return bucket index\n      end\n      # Record the first removal marker encountered\n      first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE\n      # Calculate bucket index, wrap around to the head if past the tail\n      index = (index + 1) % @capacity\n    end\n    # If key does not exist, return the index for insertion\n    first_tombstone == -1 ? index : first_tombstone\n  end\n\n  ### Query operation ###\n  def get(key)\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair is found, return corresponding val\n    return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n    # Return nil if key-value pair does not exist\n    nil\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    # When load factor exceeds threshold, perform expansion\n    extend if load_factor > @load_thres\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair found, overwrite val and return\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index].val = val\n      return\n    end\n    # If key-value pair does not exist, add the key-value pair\n    @buckets[index] = Pair.new(key, val)\n    @size += 1\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair is found, overwrite it with removal marker\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index] = TOMBSTONE\n      @size -= 1\n    end\n  end\n\n  ### Expand hash table ###\n  def extend\n    # Temporarily store the original hash table\n    buckets_tmp = @buckets\n    # Initialize expanded new hash table\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity)\n    @size = 0\n    # Move key-value pairs from original hash table to new hash table\n    for pair in buckets_tmp\n      put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n    end\n  end\n\n  ### Print hash table ###\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":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2-quadratic-probing","level":3,"title":"2.   Quadratic Probing","text":"

    Quadratic probing is similar to linear probing and is one of the common strategies for open addressing. When a collision occurs, quadratic probing does not simply skip a fixed number of steps but skips a number of steps equal to the \"square of the number of probes\", i.e., \\(1, 4, 9, \\dots\\) steps.

    Quadratic probing has the following advantages:

    • Quadratic probing attempts to alleviate the clustering effect of linear probing by skipping distances equal to the square of the probe count.
    • Quadratic probing skips larger distances to find empty positions, which helps to distribute data more evenly.

    However, quadratic probing is not perfect:

    • Clustering still exists, i.e., some positions are more likely to be occupied than others.
    • Due to the growth of squares, quadratic probing may not probe the entire hash table, meaning that even if there are empty buckets in the hash table, quadratic probing may not be able to access them.
    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3-multiple-hashing","level":3,"title":"3.   Multiple Hashing","text":"

    As the name suggests, multiple hashing uses multiple hash functions \\(f_1(x)\\), \\(f_2(x)\\), \\(f_3(x)\\), \\(\\dots\\) for probing.

    • Inserting elements: If hash function \\(f_1(x)\\) encounters a conflict, try \\(f_2(x)\\), and so on, until an empty position is found and the element is inserted.
    • Searching for elements: Search in the same order of hash functions until the target element is found and return it; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return None.

    Compared with linear probing, multiple hashing is less prone to clustering, but using multiple hash functions introduces additional computational overhead.

    Tip

    Please note that hash tables based on open addressing, including linear probing, quadratic probing, and multiple hashing, all have the problem that elements cannot be deleted directly.

    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623-choice-of-programming-languages","level":2,"title":"6.2.3   Choice of Programming Languages","text":"

    Different programming languages adopt different hash table implementation strategies. Here are a few examples:

    • Python uses open addressing. The dict dictionary uses pseudo-random numbers for probing.
    • Java uses separate chaining. Since JDK 1.8, when the array length in HashMap reaches 64 and the length of a linked list reaches 8, the linked list is converted to a red-black tree to improve search performance.
    • Go uses separate chaining. Go stipulates that each bucket can store up to 8 key-value pairs, and if the capacity is exceeded, an overflow bucket is linked; when there are too many overflow buckets, a special equal-capacity expansion operation is performed to ensure performance.
    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   Hash Table","text":"

    A hash table, also known as a hash map, stores mappings from keys key to values value, enabling efficient lookups. Specifically, given a key key, we can retrieve the corresponding value value from a hash table in \\(O(1)\\) time.

    As shown below, suppose we have \\(n\\) students, each with two pieces of information: a name and a student ID. If we want to support the query \"given a student ID, return the corresponding name,\" we can use the hash table shown below.

    Figure 6-1   Abstract representation of a hash table

    In addition to hash tables, arrays and linked lists can also implement query functionality. Their efficiency comparison is shown in the following table.

    • Adding elements: Simply add elements to the end of the array (linked list), using \\(O(1)\\) time.
    • Querying elements: Since the array (linked list) is unordered, all elements need to be traversed, using \\(O(n)\\) time.
    • Deleting elements: The element must first be located, then deleted from the array (linked list), using \\(O(n)\\) time.

    Table 6-1   Comparison of element query efficiency

    Array Linked List Hash Table Find element \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) Add element \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Delete element \\(O(n)\\) \\(O(n)\\) \\(O(1)\\)

    As we can see, insertion, deletion, lookup, and update operations in a hash table all have time complexity \\(O(1)\\), making hash tables highly efficient.

    ","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#611-common-hash-table-operations","level":2,"title":"6.1.1   Common Hash Table Operations","text":"

    Common operations on hash tables include: initialization, query operations, adding key-value pairs, and deleting key-value pairs. Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
    # Initialize hash table\nhmap: dict = {}\n\n# Add operation\n# Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n# Query operation\n# Input key into hash table to get value\nname: str = hmap[15937]\n\n# Delete operation\n# Delete key-value pair (key, value) from hash table\nhmap.pop(10583)\n
    hash_map.cpp
    /* Initialize hash table */\nunordered_map<int, string> map;\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\";\nmap[15937] = \"XiaoLuo\";\nmap[16750] = \"XiaoSuan\";\nmap[13276] = \"XiaoFa\";\nmap[10583] = \"XiaoYa\";\n\n/* Query operation */\n// Input key into hash table to get value\nstring name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.erase(10583);\n
    hash_map.java
    /* Initialize hash table */\nMap<Integer, String> map = new HashMap<>();\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.put(12836, \"XiaoHa\");\nmap.put(15937, \"XiaoLuo\");\nmap.put(16750, \"XiaoSuan\");\nmap.put(13276, \"XiaoFa\");\nmap.put(10583, \"XiaoYa\");\n\n/* Query operation */\n// Input key into hash table to get value\nString name = map.get(15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583);\n
    hash_map.cs
    /* Initialize hash table */\nDictionary<int, string> map = new() {\n    /* Add operation */\n    // Add key-value pair (key, value) to hash table\n    { 12836, \"XiaoHa\" },\n    { 15937, \"XiaoLuo\" },\n    { 16750, \"XiaoSuan\" },\n    { 13276, \"XiaoFa\" },\n    { 10583, \"XiaoYa\" }\n};\n\n/* Query operation */\n// Input key into hash table to get value\nstring name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.Remove(10583);\n
    hash_map_test.go
    /* Initialize hash table */\nhmap := make(map[int]string)\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nname := hmap[15937]\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\ndelete(hmap, 10583)\n
    hash_map.swift
    /* Initialize hash table */\nvar map: [Int: String] = [:]\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\"\nmap[15937] = \"XiaoLuo\"\nmap[16750] = \"XiaoSuan\"\nmap[13276] = \"XiaoFa\"\nmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map[15937]!\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.removeValue(forKey: 10583)\n
    hash_map.js
    /* Initialize hash table */\nconst map = new Map();\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.set(12836, 'XiaoHa');\nmap.set(15937, 'XiaoLuo');\nmap.set(16750, 'XiaoSuan');\nmap.set(13276, 'XiaoFa');\nmap.set(10583, 'XiaoYa');\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map.get(15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.delete(10583);\n
    hash_map.ts
    /* Initialize hash table */\nconst map = new Map<number, string>();\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.set(12836, 'XiaoHa');\nmap.set(15937, 'XiaoLuo');\nmap.set(16750, 'XiaoSuan');\nmap.set(13276, 'XiaoFa');\nmap.set(10583, 'XiaoYa');\nconsole.info('\\nAfter adding, hash table is\\nKey -> Value');\nconsole.info(map);\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map.get(15937);\nconsole.info('\\nInput student ID 15937, queried name ' + name);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.delete(10583);\nconsole.info('\\nAfter deleting 10583, hash table is\\nKey -> Value');\nconsole.info(map);\n
    hash_map.dart
    /* Initialize hash table */\nMap<int, String> map = {};\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\";\nmap[15937] = \"XiaoLuo\";\nmap[16750] = \"XiaoSuan\";\nmap[13276] = \"XiaoFa\";\nmap[10583] = \"XiaoYa\";\n\n/* Query operation */\n// Input key into hash table to get value\nString name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583);\n
    hash_map.rs
    use std::collections::HashMap;\n\n/* Initialize hash table */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.insert(12836, \"XiaoHa\".to_string());\nmap.insert(15937, \"XiaoLuo\".to_string());\nmap.insert(16750, \"XiaoSuan\".to_string());\nmap.insert(13276, \"XiaoFa\".to_string());\nmap.insert(10583, \"XiaoYa\".to_string());\n\n/* Query operation */\n// Input key into hash table to get value\nlet _name: Option<&String> = map.get(&15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nlet _removed_value: Option<String> = map.remove(&10583);\n
    hash_map.c
    // C does not provide a built-in hash table\n
    hash_map.kt
    /* Initialize hash table */\nval map = HashMap<Int,String>()\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\"\nmap[15937] = \"XiaoLuo\"\nmap[16750] = \"XiaoSuan\"\nmap[13276] = \"XiaoFa\"\nmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nval name = map[15937]\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583)\n
    hash_map.rb
    # Initialize hash table\nhmap = {}\n\n# Add operation\n# Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n# Query operation\n# Input key into hash table to get value\nname = hmap[15937]\n\n# Delete operation\n# Delete key-value pair (key, value) from hash table\nhmap.delete(10583)\n
    Visualized Execution

    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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%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

    There are three common ways to traverse a hash table: traversing key-value pairs, traversing keys, and traversing values. Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
    # Traverse hash table\n# Traverse key-value pairs key->value\nfor key, value in hmap.items():\n    print(key, \"->\", value)\n# Traverse keys only\nfor key in hmap.keys():\n    print(key)\n# Traverse values only\nfor value in hmap.values():\n    print(value)\n
    hash_map.cpp
    /* Traverse hash table */\n// Traverse key-value pairs key->value\nfor (auto kv: map) {\n    cout << kv.first << \" -> \" << kv.second << endl;\n}\n// Traverse using iterator key->value\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n    cout << iter->first << \"->\" << iter->second << endl;\n}\n
    hash_map.java
    /* Traverse hash table */\n// Traverse key-value pairs key->value\nfor (Map.Entry<Integer, String> kv: map.entrySet()) {\n    System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// Traverse keys only\nfor (int key: map.keySet()) {\n    System.out.println(key);\n}\n// Traverse values only\nfor (String val: map.values()) {\n    System.out.println(val);\n}\n
    hash_map.cs
    /* Traverse hash table */\n// Traverse key-value pairs Key->Value\nforeach (var kv in map) {\n    Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// Traverse keys only\nforeach (int key in map.Keys) {\n    Console.WriteLine(key);\n}\n// Traverse values only\nforeach (string val in map.Values) {\n    Console.WriteLine(val);\n}\n
    hash_map_test.go
    /* Traverse hash table */\n// Traverse key-value pairs key->value\nfor key, value := range hmap {\n    fmt.Println(key, \"->\", value)\n}\n// Traverse keys only\nfor key := range hmap {\n    fmt.Println(key)\n}\n// Traverse values only\nfor _, value := range hmap {\n    fmt.Println(value)\n}\n
    hash_map.swift
    /* Traverse hash table */\n// Traverse key-value pairs Key->Value\nfor (key, value) in map {\n    print(\"\\(key) -> \\(value)\")\n}\n// Traverse keys only\nfor key in map.keys {\n    print(key)\n}\n// Traverse values only\nfor value in map.values {\n    print(value)\n}\n
    hash_map.js
    /* Traverse hash table */\nconsole.info('\\nTraverse key-value pairs Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nTraverse keys only Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nTraverse values only Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
    hash_map.ts
    /* Traverse hash table */\nconsole.info('\\nTraverse key-value pairs Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nTraverse keys only Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nTraverse values only Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
    hash_map.dart
    /* Traverse hash table */\n// Traverse key-value pairs Key->Value\nmap.forEach((key, value) {\n  print('$key -> $value');\n});\n\n// Traverse keys only\nmap.keys.forEach((key) {\n  print(key);\n});\n\n// Traverse values only\nmap.values.forEach((value) {\n  print(value);\n});\n
    hash_map.rs
    /* Traverse hash table */\n// Traverse key-value pairs Key->Value\nfor (key, value) in &map {\n    println!(\"{key} -> {value}\");\n}\n\n// Traverse keys only\nfor key in map.keys() {\n    println!(\"{key}\");\n}\n\n// Traverse values only\nfor value in map.values() {\n    println!(\"{value}\");\n}\n
    hash_map.c
    // C does not provide a built-in hash table\n
    hash_map.kt
    /* Traverse hash table */\n// Traverse key-value pairs key->value\nfor ((key, value) in map) {\n    println(\"$key -> $value\")\n}\n// Traverse keys only\nfor (key in map.keys) {\n    println(key)\n}\n// Traverse values only\nfor (_val in map.values) {\n    println(_val)\n}\n
    hash_map.rb
    # Traverse hash table\n# Traverse key-value pairs key->value\nhmap.entries.each { |key, value| puts \"#{key} -> #{value}\" }\n\n# Traverse keys only\nhmap.keys.each { |key| puts key }\n\n# Traverse values only\nhmap.values.each { |val| puts val }\n
    Visualized Execution

    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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%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%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%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":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#612-simple-hash-table-implementation","level":2,"title":"6.1.2   Simple Hash Table Implementation","text":"

    Let's start with the simplest case: implementing a hash table with just an array. In a hash table, each empty slot in the array is called a bucket, and each bucket can store one key-value pair. A lookup therefore consists of finding the bucket for key and reading the value stored there.

    So how do we find the right bucket for a given key? We do this with a hash function. A hash function maps a larger input space to a smaller output space. In a hash table, the input space is the set of all keys, and the output space is the set of all buckets (array indices). In other words, given a key, the hash function tells us where the corresponding key-value pair should be stored in the array.

    Given a key, computing the bucket index involves the following two steps:

    1. Use a hash algorithm hash() to compute a hash value.
    2. Take that hash value modulo the number of buckets (array length), capacity, to obtain the bucket (array index) index corresponding to the key.
    index = hash(key) % capacity\n

    We can then use index to access the corresponding bucket in the hash table and retrieve the value.

    Suppose the array length is capacity = 100 and the hash algorithm is hash(key) = key. Then the hash function is key % 100. Figure 6-2 illustrates how this hash function works, using student ID as key and name as value.

    Figure 6-2   Working principle of hash function

    The following code implements a simple hash table. Here, we encapsulate key and value into a class Pair to represent a key-value pair.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py
    class Pair:\n    \"\"\"Key-value pair\"\"\"\n\n    def __init__(self, key: int, val: str):\n        self.key = key\n        self.val = val\n\nclass ArrayHashMap:\n    \"\"\"Hash table based on array implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        # Initialize array with 100 buckets\n        self.buckets: list[Pair | None] = [None] * 100\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        index = key % 100\n        return index\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Query operation\"\"\"\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        \"\"\"Add and update operation\"\"\"\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        \"\"\"Remove operation\"\"\"\n        index: int = self.hash_func(key)\n        # Set to None to represent removal\n        self.buckets[index] = None\n\n    def entry_set(self) -> list[Pair]:\n        \"\"\"Get all key-value pairs\"\"\"\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        \"\"\"Get all keys\"\"\"\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        \"\"\"Get all values\"\"\"\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        \"\"\"Print hash table\"\"\"\n        for pair in self.buckets:\n            if pair is not None:\n                print(pair.key, \"->\", pair.val)\n
    array_hash_map.cpp
    /* Key-value pair */\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/* Hash table based on array implementation */\nclass ArrayHashMap {\n  private:\n    vector<Pair *> buckets;\n\n  public:\n    ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = vector<Pair *>(100);\n    }\n\n    ~ArrayHashMap() {\n        // Free memory\n        for (const auto &bucket : buckets) {\n            delete bucket;\n        }\n        buckets.clear();\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        // Free memory and set to nullptr\n        delete buckets[index];\n        buckets[index] = nullptr;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\n    void print() {\n        for (Pair *kv : pairSet()) {\n            cout << kv->key << \" -> \" << kv->val << endl;\n        }\n    }\n};\n
    array_hash_map.java
    /* Key-value pair */\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/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private List<Pair> buckets;\n\n    public ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = new ArrayList<>();\n        for (int i = 0; i < 100; i++) {\n            buckets.add(null);\n        }\n    }\n\n    /* Hash function */\n    private int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    public void remove(int key) {\n        int index = hashFunc(key);\n        // Set to null to represent deletion\n        buckets.set(index, null);\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair int->string */\nclass Pair(int key, string val) {\n    public int key = key;\n    public string val = val;\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    List<Pair?> buckets;\n    public ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = [];\n        for (int i = 0; i < 100; i++) {\n            buckets.Add(null);\n        }\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Set to null to represent deletion\n        buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair */\ntype pair struct {\n    key int\n    val string\n}\n\n/* Hash table based on array implementation */\ntype arrayHashMap struct {\n    buckets []*pair\n}\n\n/* Initialize hash table */\nfunc newArrayHashMap() *arrayHashMap {\n    // Initialize array with 100 buckets\n    buckets := make([]*pair, 100)\n    return &arrayHashMap{buckets: buckets}\n}\n\n/* Hash function */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n    index := key % 100\n    return index\n}\n\n/* Query operation */\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/* Add operation */\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/* Remove operation */\nfunc (a *arrayHashMap) remove(key int) {\n    index := a.hashFunc(key)\n    // Set to nil to delete\n    a.buckets[index] = nil\n}\n\n/* Get all key pairs */\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/* Get all keys */\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/* Get all values */\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/* Print hash table */\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
    /* Key-value pair */\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/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private var buckets: [Pair?]\n\n    init() {\n        // Initialize array with 100 buckets\n        buckets = Array(repeating: nil, count: 100)\n    }\n\n    /* Hash function */\n    private func hashFunc(key: Int) -> Int {\n        let index = key % 100\n        return index\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let pair = buckets[index]\n        return pair?.val\n    }\n\n    /* Add operation */\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    /* Remove operation */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        // Set to nil to delete\n        buckets[index] = nil\n    }\n\n    /* Get all key-value pairs */\n    func pairSet() -> [Pair] {\n        buckets.compactMap { $0 }\n    }\n\n    /* Get all keys */\n    func keySet() -> [Int] {\n        buckets.compactMap { $0?.key }\n    }\n\n    /* Get all values */\n    func valueSet() -> [String] {\n        buckets.compactMap { $0?.val }\n    }\n\n    /* Print hash table */\n    func print() {\n        for pair in pairSet() {\n            Swift.print(\"\\(pair.key) -> \\(pair.val)\")\n        }\n    }\n}\n
    array_hash_map.js
    /* Key-value pair Number -> String */\nclass Pair {\n    constructor(key, val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    #buckets;\n    constructor() {\n        // Initialize array with 100 buckets\n        this.#buckets = new Array(100).fill(null);\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % 100;\n    }\n\n    /* Query operation */\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    /* Add operation */\n    set(key, val) {\n        let index = this.#hashFunc(key);\n        this.#buckets[index] = new Pair(key, val);\n    }\n\n    /* Remove operation */\n    delete(key) {\n        let index = this.#hashFunc(key);\n        // Set to null to represent deletion\n        this.#buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair 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/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private readonly buckets: (Pair | null)[];\n\n    constructor() {\n        // Initialize array with 100 buckets\n        this.buckets = new Array(100).fill(null);\n    }\n\n    /* Hash function */\n    private hashFunc(key: number): number {\n        return key % 100;\n    }\n\n    /* Query operation */\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    /* Add operation */\n    public set(key: number, val: string) {\n        let index = this.hashFunc(key);\n        this.buckets[index] = new Pair(key, val);\n    }\n\n    /* Remove operation */\n    public delete(key: number) {\n        let index = this.hashFunc(key);\n        // Set to null to represent deletion\n        this.buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair */\nclass Pair {\n  int key;\n  String val;\n  Pair(this.key, this.val);\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n  late List<Pair?> _buckets;\n\n  ArrayHashMap() {\n    // Initialize array with 100 buckets\n    _buckets = List.filled(100, null);\n  }\n\n  /* Hash function */\n  int _hashFunc(int key) {\n    final int index = key % 100;\n    return index;\n  }\n\n  /* Query operation */\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  /* Add operation */\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  /* Remove operation */\n  void remove(int key) {\n    final int index = _hashFunc(key);\n    _buckets[index] = null;\n  }\n\n  /* Get all key-value pairs */\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  /* Get all keys */\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  /* Get all values */\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  /* Print hash table */\n  void printHashMap() {\n    for (final Pair kv in pairSet()) {\n      print(\"${kv.key} -> ${kv.val}\");\n    }\n  }\n}\n
    array_hash_map.rs
    /* Key-value pair */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n    pub key: i32,\n    pub val: String,\n}\n\n/* Hash table based on array implementation */\npub struct ArrayHashMap {\n    buckets: Vec<Option<Pair>>,\n}\n\nimpl ArrayHashMap {\n    pub fn new() -> ArrayHashMap {\n        // Initialize array with 100 buckets\n        Self {\n            buckets: vec![None; 100],\n        }\n    }\n\n    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % 100\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    pub fn remove(&mut self, key: i32) {\n        let index = self.hash_func(key);\n        // Set to None to represent removal\n        self.buckets[index] = None;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair int->string */\ntypedef struct {\n    int key;\n    char *val;\n} Pair;\n\n/* Hash table based on array implementation */\ntypedef struct {\n    Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* Constructor */\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/* Destructor */\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/* Add operation */\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/* Remove operation */\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/* Get all key-value pairs */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n    Pair *entries;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\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/* Get all keys */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n    int *keys;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\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/* Get all values */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n    char **vals;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\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/* Print hash table */\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
    /* Key-value pair */\nclass Pair(\n    var key: Int,\n    var _val: String\n)\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    // Initialize array with 100 buckets\n    private val buckets = arrayOfNulls<Pair>(100)\n\n    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        val index = key % 100\n        return index\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        // Set to null to represent deletion\n        buckets[index] = null\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    ### Key-value pair ###\nclass Pair\n  attr_accessor :key, :val\n\n  def initialize(key, val)\n    @key = key\n    @val = val\n  end\nend\n\n### Hash map based on array ###\nclass ArrayHashMap\n  ### Constructor ###\n  def initialize\n    # Initialize array with 100 buckets\n    @buckets = Array.new(100)\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    index = key % 100\n  end\n\n  ### Query operation ###\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  ### Add operation ###\n  def put(key, val)\n    pair = Pair.new(key, val)\n    index = hash_func(key)\n    @buckets[index] = pair\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    index = hash_func(key)\n    # Set to nil to delete\n    @buckets[index] = nil\n  end\n\n  ### Get all key-value pairs ###\n  def entry_set\n    result = []\n    @buckets.each { |pair| result << pair unless pair.nil? }\n    result\n  end\n\n  ### Get all keys ###\n  def key_set\n    result = []\n    @buckets.each { |pair| result << pair.key unless pair.nil? }\n    result\n  end\n\n  ### Get all values ###\n  def value_set\n    result = []\n    @buckets.each { |pair| result << pair.val unless pair.nil? }\n    result\n  end\n\n  ### Print hash table ###\n  def print\n    @buckets.each { |pair| puts \"#{pair.key} -> #{pair.val}\" unless pair.nil? }\n  end\nend\n
    ","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#613-hash-collision-and-resizing","level":2,"title":"6.1.3   Hash Collision and Resizing","text":"

    Fundamentally, a hash function maps the input space consisting of all keys to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, in theory, different inputs must sometimes map to the same output.

    For the hash function in the above example, when the input keys have the same last two digits, the hash function produces the same output. For example, when querying two students with IDs 12836 and 20336, we get:

    12836 % 100 = 36\n20336 % 100 = 36\n

    As shown below, two student IDs now point to the same name, which is clearly incorrect. We call this situation, where multiple inputs map to the same output, a hash collision.

    Figure 6-3   Hash collision example

    It's easy to see that the larger the hash table capacity \\(n\\), the lower the probability that multiple keys will be assigned to the same bucket, and the fewer collisions. Therefore, we can reduce hash collisions by expanding the hash table.

    As shown in Figure 6-4, before expansion, the key-value pairs (136, A) and (236, D) collided, but after expansion, the collision disappears.

    Figure 6-4   Hash table resizing

    Like resizing an array, resizing a hash table requires migrating all key-value pairs from the original table to the new table, which is expensive. In addition, because the hash table capacity capacity changes, we must recompute the storage location of every key-value pair using the hash function, which further increases the cost of resizing. For this reason, programming languages typically reserve a sufficiently large hash table capacity to avoid frequent resizing.

    The load factor is an important concept in hash tables. It is defined as the number of elements in the hash table divided by the number of buckets and is used to measure the severity of hash collisions. It is also commonly used as a threshold for triggering hash table resizing. For example, in Java, when the load factor exceeds \\(0.75\\), the system expands the hash table to twice its original size.

    ","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   Summary","text":"","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_hashing/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Given an input key, a hash table can retrieve the corresponding value in \\(O(1)\\) time, which is highly efficient.
    • Common hash table operations include querying, adding key-value pairs, deleting key-value pairs, and traversing the hash table.
    • The hash function maps a key to an array index, allowing access to the corresponding bucket and retrieval of the value.
    • Two different keys may end up with the same array index after hashing, leading to erroneous query results. This phenomenon is known as hash collision.
    • The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table expansion can mitigate hash collisions. Similar to array expansion, hash table expansion is costly.
    • The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table expansion.
    • Separate chaining addresses hash collisions by storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by further converting the linked lists into red-black trees.
    • Open addressing handles hash collisions through multiple probing. Linear probing uses a fixed step size but cannot delete elements and is prone to clustering. Double hashing uses multiple hash functions for probing, which reduces clustering compared to linear probing but increases computational overhead.
    • Different programming languages adopt various hash table implementations. For example, Java's HashMap uses separate chaining, while Python's dict employs open addressing.
    • In hash tables, we desire hash algorithms with determinism, high efficiency, and uniform distribution. In cryptography, hash algorithms should also possess collision resistance and the avalanche effect.
    • Hash algorithms typically use large prime numbers as moduli to maximize the uniform distribution of hash values and reduce hash collisions.
    • Common hash algorithms include MD5, SHA-1, SHA-2, and SHA-3. MD5 is often used for file integrity checks, while SHA-2 is commonly used in secure applications and protocols.
    • Programming languages usually provide built-in hash algorithms for data types to calculate bucket indices in hash tables. Generally, only immutable objects are hashable.
    ","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: When does the time complexity of a hash table degrade to \\(O(n)\\)?

    The time complexity of a hash table can degrade to \\(O(n)\\) when hash collisions are severe. When the hash function is well-designed, the capacity is set appropriately, and collisions are evenly distributed, the time complexity is \\(O(1)\\). We usually consider the time complexity to be \\(O(1)\\) when using built-in hash tables in programming languages.

    Q: Why not use the hash function \\(f(x) = x\\)? This would eliminate collisions.

    Under the hash function \\(f(x) = x\\), each element corresponds to a unique bucket index, which is equivalent to an array. However, the input space is usually much larger than the output space (array length), so the last step of a hash function is often to take the modulo of the array length. In other words, the goal of a hash table is to map a larger state space to a smaller one while providing \\(O(1)\\) query efficiency.

    Q: Why can hash tables be more efficient than arrays, linked lists, or binary trees, even though hash tables are implemented using these structures?

    Firstly, hash tables have higher time efficiency but lower space efficiency. A significant portion of memory in hash tables remains unused.

    Secondly, hash tables are only more time-efficient in specific use cases. If a feature can be implemented with the same time complexity using an array or a linked list, it's usually faster than using a hash table. This is because the computation of the hash function incurs overhead, making the constant factor in the time complexity larger.

    Lastly, the time complexity of hash tables can degrade. For example, in separate chaining, we perform search operations in a linked list or red-black tree, which still risks degrading to \\(O(n)\\) time.

    Q: Does double hashing also have the flaw of not being able to delete elements directly? Can space marked as deleted be reused?

    Double hashing is a form of open addressing, and all open addressing methods have the drawback of not being able to delete elements directly; they require marking elements as deleted. Marked spaces can be reused. When inserting new elements into the hash table, and the hash function points to a position marked as deleted, that position can be used by the new element. This maintains the probing sequence of the hash table while ensuring efficient use of space.

    Q: Why do hash collisions occur during the search process in linear probing?

    During the search process, the hash function points to the corresponding bucket and key-value pair. If the key doesn't match, it indicates a hash collision. Therefore, linear probing will search downward at a predetermined step size until the correct key-value pair is found or the search fails.

    Q: Why can expanding a hash table alleviate hash collisions?

    The last step of a hash function often involves taking the modulo of the array length \\(n\\), to keep the output within the array index range. When expanding, the array length \\(n\\) changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after expansion, thereby mitigating hash collisions.

    Q: If the goal is efficient access, why not just use an array directly?

    When the key values are continuous integers within a small range, an array is indeed a simple and efficient choice. But when the key is of another type, such as a string, we need a hash function to map the key to an array index and then store the element in a bucket array. That structure is precisely what a hash table is.

    ","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"Chapter 8.   Heap","text":"

    Abstract

    Heaps are like mountain peaks, rising layer upon layer, each with a distinct shape.

    The peaks rise and fall at varying heights, yet the tallest peak always catches the eye first.

    ","path":["Chapter 8. Heap","Chapter 8.   Heap"],"tags":[]},{"location":"chapter_heap/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 8.1   Heap
    • 8.2   Heap Construction Operation
    • 8.3   Top-k Problem
    • 8.4   Summary
    ","path":["Chapter 8. Heap","Chapter 8.   Heap"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   Heap Construction Operation","text":"

    In some cases, we want to build a heap using all elements of a list, and this process is called \"heap construction operation.\"

    ","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#821-implementing-with-element-insertion","level":2,"title":"8.2.1   Implementing with Element Insertion","text":"

    We first create an empty heap, then iterate through the list, performing the \"element insertion operation\" on each element in sequence. This means appending the element to the end of the heap and then performing \"bottom-to-top\" heapify on that element.

    Each time an element is inserted into the heap, the heap's length increases by one. Since nodes are added to the binary tree sequentially from top to bottom, the heap is constructed \"from top to bottom.\"

    Given \\(n\\) elements, each element's insertion operation takes \\(O(\\log{n})\\) time, so the time complexity of this heap construction method is \\(O(n \\log n)\\).

    ","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#822-implementing-through-heapify-traversal","level":2,"title":"8.2.2   Implementing Through Heapify Traversal","text":"

    In fact, we can implement a more efficient heap construction method in two steps.

    1. Add all elements of the list as-is to the heap, at which point the heap property is not yet satisfied.
    2. Traverse the heap in reverse order (reverse of level-order traversal), performing \"top-to-bottom heapify\" on each non-leaf node in sequence.

    After heapifying a node, the subtree rooted at that node becomes a valid sub-heap. Since we traverse in reverse order, the heap is constructed \"from bottom to top.\"

    The reason for choosing reverse-order traversal is that it ensures the subtrees beneath the current node are already valid sub-heaps, so heapifying the current node is effective.

    It's worth noting that since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification. As shown in the code below, the last non-leaf node is the parent of the last node; we start from that node and heapify while traversing in reverse order:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def __init__(self, nums: list[int]):\n    \"\"\"Constructor, build heap based on input list\"\"\"\n    # Add list elements to heap as is\n    self.max_heap = nums\n    # Heapify all nodes except leaf nodes\n    for i in range(self.parent(self.size() - 1), -1, -1):\n        self.sift_down(i)\n
    my_heap.cpp
    /* Constructor, build heap based on input list */\nMaxHeap(vector<int> nums) {\n    // Add list elements to heap as is\n    maxHeap = nums;\n    // Heapify all nodes except leaf nodes\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
    my_heap.java
    /* Constructor, build heap based on input list */\nMaxHeap(List<Integer> nums) {\n    // Add list elements to heap as is\n    maxHeap = new ArrayList<>(nums);\n    // Heapify all nodes except leaf nodes\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
    my_heap.cs
    /* Constructor, build heap from input list */\nMaxHeap(IEnumerable<int> nums) {\n    // Add list elements to heap as is\n    maxHeap = new List<int>(nums);\n    // Heapify all nodes except leaf nodes\n    var size = Parent(this.Size() - 1);\n    for (int i = size; i >= 0; i--) {\n        SiftDown(i);\n    }\n}\n
    my_heap.go
    /* Constructor, build heap from slice */\nfunc newMaxHeap(nums []any) *maxHeap {\n    // Add list elements to heap as is\n    h := &maxHeap{data: nums}\n    for i := h.parent(len(h.data) - 1); i >= 0; i-- {\n        // Heapify all nodes except leaf nodes\n        h.siftDown(i)\n    }\n    return h\n}\n
    my_heap.swift
    /* Constructor, build heap based on input list */\ninit(nums: [Int]) {\n    // Add list elements to heap as is\n    maxHeap = nums\n    // Heapify all nodes except leaf nodes\n    for i in (0 ... parent(i: size() - 1)).reversed() {\n        siftDown(i: i)\n    }\n}\n
    my_heap.js
    /* Constructor, build empty heap or build heap from input list */\nconstructor(nums) {\n    // Add list elements to heap as is\n    this.#maxHeap = nums === undefined ? [] : [...nums];\n    // Heapify all nodes except leaf nodes\n    for (let i = this.#parent(this.size() - 1); i >= 0; i--) {\n        this.#siftDown(i);\n    }\n}\n
    my_heap.ts
    /* Constructor, build empty heap or build heap from input list */\nconstructor(nums?: number[]) {\n    // Add list elements to heap as is\n    this.maxHeap = nums === undefined ? [] : [...nums];\n    // Heapify all nodes except leaf nodes\n    for (let i = this.parent(this.size() - 1); i >= 0; i--) {\n        this.siftDown(i);\n    }\n}\n
    my_heap.dart
    /* Constructor, build heap based on input list */\nMaxHeap(List<int> nums) {\n  // Add list elements to heap as is\n  _maxHeap = nums;\n  // Heapify all nodes except leaf nodes\n  for (int i = _parent(size() - 1); i >= 0; i--) {\n    siftDown(i);\n  }\n}\n
    my_heap.rs
    /* Constructor, build heap based on input list */\nfn new(nums: Vec<i32>) -> Self {\n    // Add list elements to heap as is\n    let mut heap = MaxHeap { max_heap: nums };\n    // Heapify all nodes except leaf nodes\n    for i in (0..=Self::parent(heap.size() - 1)).rev() {\n        heap.sift_down(i);\n    }\n    heap\n}\n
    my_heap.c
    /* Constructor, build heap from slice */\nMaxHeap *newMaxHeap(int nums[], int size) {\n    // Push all elements to heap\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 all nodes except leaf nodes\n        siftDown(maxHeap, i);\n    }\n    return maxHeap;\n}\n
    my_heap.kt
    /* Max heap */\nclass MaxHeap(nums: MutableList<Int>?) {\n    // Use list instead of array, no need to consider capacity expansion\n    private val maxHeap = mutableListOf<Int>()\n\n    /* Constructor, build heap based on input list */\n    init {\n        // Add list elements to heap as is\n        maxHeap.addAll(nums!!)\n        // Heapify all nodes except leaf nodes\n        for (i in parent(size() - 1) downTo 0) {\n            siftDown(i)\n        }\n    }\n\n    /* Get index of left child node */\n    private fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Get index of right child node */\n    private fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Get index of parent node */\n    private fun parent(i: Int): Int {\n        return (i - 1) / 2 // Floor division\n    }\n\n    /* Swap elements */\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    /* Get heap size */\n    fun size(): Int {\n        return maxHeap.size\n    }\n\n    /* Check if heap is empty */\n    fun isEmpty(): Boolean {\n        /* Check if heap is empty */\n        return size() == 0\n    }\n\n    /* Access top element */\n    fun peek(): Int {\n        return maxHeap[0]\n    }\n\n    /* Element enters heap */\n    fun push(_val: Int) {\n        // Add node\n        maxHeap.add(_val)\n        // Heapify from bottom to top\n        siftUp(size() - 1)\n    }\n\n    /* Starting from node i, heapify from bottom to top */\n    private fun siftUp(it: Int) {\n        // Kotlin function parameters are immutable, so create temporary variable\n        var i = it\n        while (true) {\n            // Get parent node of node i\n            val p = parent(i)\n            // When \"crossing root node\" or \"node needs no repair\", end heapify\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // Swap two nodes\n            swap(i, p)\n            // Loop upward heapify\n            i = p\n        }\n    }\n\n    /* Element exits heap */\n    fun pop(): Int {\n        // Handle empty case\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Delete node\n        swap(0, size() - 1)\n        // Remove node\n        val _val = maxHeap.removeAt(size() - 1)\n        // Return top element\n        siftDown(0)\n        // Return heap top element\n        return _val\n    }\n\n    /* Starting from node i, heapify from top to bottom */\n    private fun siftDown(it: Int) {\n        // Kotlin function parameters are immutable, so create temporary variable\n        var i = it\n        while (true) {\n            // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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            // Swap two nodes\n            if (ma == i) break\n            // Swap two nodes\n            swap(i, ma)\n            // Loop downwards heapification\n            i = ma\n        }\n    }\n\n    /* Driver Code */\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
    ### Constructor, build heap from input list ###\ndef initialize(nums)\n  # Add list elements to heap as is\n  @max_heap = nums\n  # Heapify all nodes except leaf nodes\n  parent(size - 1).downto(0) do |i|\n    sift_down(i)\n  end\nend\n
    ","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#823-complexity-analysis","level":2,"title":"8.2.3   Complexity Analysis","text":"

    Next, let's attempt to derive the time complexity of this second heap construction method.

    • Assuming the complete binary tree has \\(n\\) nodes, then the number of leaf nodes is \\((n + 1) / 2\\), where \\(/\\) is floor division. Therefore, the number of nodes that need heapification is \\((n - 1) / 2\\).
    • In the top-to-bottom heapify process, each node can sink at most to a leaf node, so the maximum number of iterations is the height of the binary tree, \\(\\log n\\).

    Multiplying these two together, we get a time complexity of \\(O(n \\log n)\\) for the heap construction process. However, this estimate is not accurate because it doesn't account for the property that binary trees have far more nodes at lower levels than at upper levels.

    Let's perform a more accurate calculation. To simplify the analysis, assume a \"perfect binary tree\" with \\(n\\) nodes and height \\(h\\); this assumption does not affect the correctness of the result.

    Figure 8-5   Node count at each level of a perfect binary tree

    As shown in Figure 8-5, the maximum number of iterations for a node's \"top-to-bottom heapify\" equals the distance from that node to a leaf node, which is precisely the node's height. Therefore, we can sum the \"number of nodes \\(\\times\\) node height\" at each level to obtain the total number of heapify iterations for all nodes.

    \\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\]

    Simplifying the expression above requires some high-school sequence algebra. First, multiply \\(T(h)\\) by \\(2\\) to get:

    \\[ \\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} \\]

    Using subtraction of shifted sums, subtract the first equation \\(T(h)\\) from the second equation \\(2 T(h)\\) to get:

    \\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\]

    Observing the above expression, we find that \\(T(h)\\) is a geometric series, which can be calculated directly using the sum formula, yielding a time complexity of:

    \\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\]

    Furthermore, a perfect binary tree with height \\(h\\) has \\(n = 2^{h+1} - 1\\) nodes, so the complexity is \\(O(2^h) = O(n)\\). This derivation shows that the time complexity of building a heap from an input list is \\(O(n)\\), which is highly efficient.

    ","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   Heap","text":"

    A heap is a complete binary tree that satisfies specific conditions and can be mainly categorized into two types, as shown in Figure 8-1.

    • min heap: The value of any node \\(\\leq\\) the values of its child nodes.
    • max heap: The value of any node \\(\\geq\\) the values of its child nodes.

    Figure 8-1   Min heap and max heap

    As a special case of a complete binary tree, heaps have the following characteristics.

    • The bottom layer nodes are filled from left to right, and nodes in other layers are fully filled.
    • We call the root node of the binary tree the \"heap top\" and the bottom-rightmost node the \"heap bottom.\"
    • For max heaps (min heaps), the value of the heap top element (root node) is the largest (smallest).
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#811-common-heap-operations","level":2,"title":"8.1.1   Common Heap Operations","text":"

    It should be noted that many programming languages provide a priority queue, an abstract data structure defined as a queue whose elements are ordered by priority.

    In fact, heaps are typically used to implement priority queues, with max heaps corresponding to priority queues where elements are dequeued in descending order. From a usage perspective, we can regard \"priority queue\" and \"heap\" as equivalent data structures. Therefore, this book does not make a special distinction between the two and uniformly refers to them as \"heap.\"

    Common heap operations are shown in Table 8-1, and method names need to be determined based on the programming language.

    Table 8-1   Efficiency of Heap Operations

    Method name Description Time complexity push() Insert an element into the heap \\(O(\\log n)\\) pop() Remove the heap top element \\(O(\\log n)\\) peek() Access the heap top element (max/min value for max/min heap) \\(O(1)\\) size() Get the number of elements in the heap \\(O(1)\\) isEmpty() Check if the heap is empty \\(O(1)\\)

    In practical applications, we can directly use the heap class (or priority queue class) provided by programming languages.

    Similar to \"ascending order\" and \"descending order\" in sorting algorithms, we can implement conversion between \"min heap\" and \"max heap\" by setting a flag or modifying the Comparator. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py
    # Initialize a min heap\nmin_heap, flag = [], 1\n# Initialize a max heap\nmax_heap, flag = [], -1\n\n# Python's heapq module implements a min heap by default\n# Consider negating elements before pushing them to the heap, which inverts the size relationship and thus implements a max heap\n# In this example, flag = 1 corresponds to a min heap, flag = -1 corresponds to a max heap\n\n# Push elements into the heap\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# Get the heap top element\npeek: int = flag * max_heap[0] # 5\n\n# Remove the heap top element\n# The removed elements will form a descending sequence\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# Get the heap size\nsize: int = len(max_heap)\n\n# Check if the heap is empty\nis_empty: bool = not max_heap\n\n# Build a heap from an input list\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n
    heap.cpp
    /* Initialize a heap */\n// Initialize a min heap\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// Initialize a max heap\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* Push elements into the heap */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* Get the heap top element */\nint peek = maxHeap.top(); // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* Get the heap size */\nint size = maxHeap.size();\n\n/* Check if the heap is empty */\nbool isEmpty = maxHeap.empty();\n\n/* Build a heap from an input list */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n
    heap.java
    /* Initialize a heap */\n// Initialize a min heap\nQueue<Integer> minHeap = new PriorityQueue<>();\n// Initialize a max heap (use lambda expression to modify Comparator)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* Push elements into the heap */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* Get the heap top element */\nint peek = maxHeap.peek(); // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* Get the heap size */\nint size = maxHeap.size();\n\n/* Check if the heap is empty */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* Build a heap from an input list */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n
    heap.cs
    /* Initialize a heap */\n// Initialize a min heap\nPriorityQueue<int, int> minHeap = new();\n// Initialize a max heap (use lambda expression to modify Comparer)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y.CompareTo(x)));\n\n/* Push elements into the heap */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* Get the heap top element */\nint peek = maxHeap.Peek();//5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.Dequeue();  // 5\npeek = maxHeap.Dequeue();  // 4\npeek = maxHeap.Dequeue();  // 3\npeek = maxHeap.Dequeue();  // 2\npeek = maxHeap.Dequeue();  // 1\n\n/* Get the heap size */\nint size = maxHeap.Count;\n\n/* Check if the heap is empty */\nbool isEmpty = maxHeap.Count == 0;\n\n/* Build a heap from an input list */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n
    heap.go
    // In Go, we can construct a max heap of integers by implementing heap.Interface\n// Implementing heap.Interface also requires implementing sort.Interface\ntype intHeap []any\n\n// Push implements the heap.Interface method for pushing an element into the heap\nfunc (h *intHeap) Push(x any) {\n    // Push and Pop use pointer receiver as parameters\n    // because they not only adjust the slice contents but also modify the slice length\n    *h = append(*h, x.(int))\n}\n\n// Pop implements the heap.Interface method for popping the heap top element\nfunc (h *intHeap) Pop() any {\n    // The element to be removed is stored at the end\n    last := (*h)[len(*h)-1]\n    *h = (*h)[:len(*h)-1]\n    return last\n}\n\n// Len is a sort.Interface method\nfunc (h *intHeap) Len() int {\n    return len(*h)\n}\n\n// Less is a sort.Interface method\nfunc (h *intHeap) Less(i, j int) bool {\n    // To implement a min heap, change this to a less-than sign\n    return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// Swap is a sort.Interface method\nfunc (h *intHeap) Swap(i, j int) {\n    (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top gets the heap top element\nfunc (h *intHeap) Top() any {\n    return (*h)[0]\n}\n\n/* Driver Code */\nfunc TestHeap(t *testing.T) {\n    /* Initialize a heap */\n    // Initialize a max heap\n    maxHeap := &intHeap{}\n    heap.Init(maxHeap)\n    /* Push elements into the heap */\n    // Call heap.Interface methods to add elements\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    /* Get the heap top element */\n    top := maxHeap.Top()\n    fmt.Printf(\"Heap top element is %d\\n\", top)\n\n    /* Remove the heap top element */\n    // Call heap.Interface methods to remove elements\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    /* Get the heap size */\n    size := len(*maxHeap)\n    fmt.Printf(\"Number of heap elements is %d\\n\", size)\n\n    /* Check if the heap is empty */\n    isEmpty := len(*maxHeap) == 0\n    fmt.Printf(\"Is the heap empty? %t\\n\", isEmpty)\n}\n
    heap.swift
    /* Initialize a heap */\n// Swift's Heap type supports both max heaps and min heaps, and requires importing swift-collections\nvar heap = Heap<Int>()\n\n/* Push elements into the heap */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* Get the heap top element */\nvar peek = heap.max()!\n\n/* Remove the heap top element */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* Get the heap size */\nlet size = heap.count\n\n/* Check if the heap is empty */\nlet isEmpty = heap.isEmpty\n\n/* Build a heap from an input list */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n
    heap.js
    // JavaScript does not provide a built-in Heap class\n
    heap.ts
    // TypeScript does not provide a built-in Heap class\n
    heap.dart
    // Dart does not provide a built-in Heap class\n
    heap.rs
    use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* Initialize a heap */\n// Initialize a min heap\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// Initialize a max heap\nlet mut max_heap = BinaryHeap::new();\n\n/* Push elements into the heap */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* Get the heap top element */\nlet peek = max_heap.peek().unwrap();  // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\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/* Get the heap size */\nlet size = max_heap.len();\n\n/* Check if the heap is empty */\nlet is_empty = max_heap.is_empty();\n\n/* Build a heap from an input list */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n
    heap.c
    // C does not provide a built-in Heap class\n
    heap.kt
    /* Initialize a heap */\n// Initialize a min heap\nvar minHeap = PriorityQueue<Int>()\n// Initialize a max heap (use lambda expression to modify Comparator)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* Push elements into the heap */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* Get the heap top element */\nvar peek = maxHeap.peek() // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* Get the heap size */\nval size = maxHeap.size\n\n/* Check if the heap is empty */\nval isEmpty = maxHeap.isEmpty()\n\n/* Build a heap from an input list */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n
    heap.rb
    # Ruby does not provide a built-in Heap class\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#812-implementation-of-the-heap","level":2,"title":"8.1.2   Implementation of the Heap","text":"

    The following implementation is for a max heap. To convert it to a min heap, simply reverse all comparison logic related to ordering (for example, replace \\(\\geq\\) with \\(\\leq\\)). Interested readers are encouraged to implement this on their own.

    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#1-heap-storage-and-representation","level":3,"title":"1.   Heap Storage and Representation","text":"

    As mentioned in the \"Binary Tree\" chapter, complete binary trees are well-suited for array representation. Since heaps are a type of complete binary tree, we will use arrays to store heaps.

    When representing a binary tree with an array, elements represent node values, and indexes represent node positions in the binary tree. Parent-child relationships are represented through index-mapping formulas.

    As shown in Figure 8-2, given an index \\(i\\), the index of its left child is \\(2i + 1\\), the index of its right child is \\(2i + 2\\), and the index of its parent is \\((i - 1) / 2\\) (floor division). When an index is out of bounds, it indicates a null node or that the node does not exist.

    Figure 8-2   Representation and storage of heaps

    We can encapsulate the index mapping formula into functions for convenient subsequent use:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def left(self, i: int) -> int:\n    \"\"\"Get index of left child node\"\"\"\n    return 2 * i + 1\n\ndef right(self, i: int) -> int:\n    \"\"\"Get index of right child node\"\"\"\n    return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n    \"\"\"Get index of parent node\"\"\"\n    return (i - 1) // 2  # Floor division\n
    my_heap.cpp
    /* Get index of left child node */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
    my_heap.java
    /* Get index of left child node */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
    my_heap.cs
    /* Get index of left child node */\nint Left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint Right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint Parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
    my_heap.go
    /* Get index of left child node */\nfunc (h *maxHeap) left(i int) int {\n    return 2*i + 1\n}\n\n/* Get index of right child node */\nfunc (h *maxHeap) right(i int) int {\n    return 2*i + 2\n}\n\n/* Get index of parent node */\nfunc (h *maxHeap) parent(i int) int {\n    // Floor division\n    return (i - 1) / 2\n}\n
    my_heap.swift
    /* Get index of left child node */\nfunc left(i: Int) -> Int {\n    2 * i + 1\n}\n\n/* Get index of right child node */\nfunc right(i: Int) -> Int {\n    2 * i + 2\n}\n\n/* Get index of parent node */\nfunc parent(i: Int) -> Int {\n    (i - 1) / 2 // Floor division\n}\n
    my_heap.js
    /* Get index of left child node */\n#left(i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\n#right(i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\n#parent(i) {\n    return Math.floor((i - 1) / 2); // Floor division\n}\n
    my_heap.ts
    /* Get index of left child node */\nleft(i: number): number {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nright(i: number): number {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nparent(i: number): number {\n    return Math.floor((i - 1) / 2); // Floor division\n}\n
    my_heap.dart
    /* Get index of left child node */\nint _left(int i) {\n  return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint _right(int i) {\n  return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint _parent(int i) {\n  return (i - 1) ~/ 2; // Floor division\n}\n
    my_heap.rs
    /* Get index of left child node */\nfn left(i: usize) -> usize {\n    2 * i + 1\n}\n\n/* Get index of right child node */\nfn right(i: usize) -> usize {\n    2 * i + 2\n}\n\n/* Get index of parent node */\nfn parent(i: usize) -> usize {\n    (i - 1) / 2 // Floor division\n}\n
    my_heap.c
    /* Get index of left child node */\nint left(MaxHeap *maxHeap, int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(MaxHeap *maxHeap, int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(MaxHeap *maxHeap, int i) {\n    return (i - 1) / 2; // Round down\n}\n
    my_heap.kt
    /* Get index of left child node */\nfun left(i: Int): Int {\n    return 2 * i + 1\n}\n\n/* Get index of right child node */\nfun right(i: Int): Int {\n    return 2 * i + 2\n}\n\n/* Get index of parent node */\nfun parent(i: Int): Int {\n    return (i - 1) / 2 // Floor division\n}\n
    my_heap.rb
    ### Get left child index ###\ndef left(i)\n  2 * i + 1\nend\n\n### Get right child index ###\ndef right(i)\n  2 * i + 2\nend\n\n### Get parent node index ###\ndef parent(i)\n  (i - 1) / 2     # Floor division\nend\n
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#2-accessing-the-heap-top-element","level":3,"title":"2.   Accessing the Heap Top Element","text":"

    The heap top element is the root node of the binary tree, which is also the first element of the list:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def peek(self) -> int:\n    \"\"\"Access top element\"\"\"\n    return self.max_heap[0]\n
    my_heap.cpp
    /* Access top element */\nint peek() {\n    return maxHeap[0];\n}\n
    my_heap.java
    /* Access top element */\nint peek() {\n    return maxHeap.get(0);\n}\n
    my_heap.cs
    /* Access top element */\nint Peek() {\n    return maxHeap[0];\n}\n
    my_heap.go
    /* Access top element */\nfunc (h *maxHeap) peek() any {\n    return h.data[0]\n}\n
    my_heap.swift
    /* Access top element */\nfunc peek() -> Int {\n    maxHeap[0]\n}\n
    my_heap.js
    /* Access top element */\npeek() {\n    return this.#maxHeap[0];\n}\n
    my_heap.ts
    /* Access top element */\npeek(): number {\n    return this.maxHeap[0];\n}\n
    my_heap.dart
    /* Access top element */\nint peek() {\n  return _maxHeap[0];\n}\n
    my_heap.rs
    /* Access top element */\nfn peek(&self) -> Option<i32> {\n    self.max_heap.first().copied()\n}\n
    my_heap.c
    /* Access top element */\nint peek(MaxHeap *maxHeap) {\n    return maxHeap->data[0];\n}\n
    my_heap.kt
    /* Access top element */\nfun peek(): Int {\n    return maxHeap[0]\n}\n
    my_heap.rb
    ### Access heap top element ###\ndef peek\n  @max_heap[0]\nend\n
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#3-inserting-an-element-into-the-heap","level":3,"title":"3.   Inserting an Element Into the Heap","text":"

    Given an element val, we first add it to the bottom of the heap. After insertion, because val may be larger than other elements in the heap, the heap property may be violated. Therefore, we need to restore the heap property along the path from the inserted node to the root. This operation is called heapify.

    Starting from the inserted node, perform heapify from bottom to top. As shown in Figure 8-3, we compare the inserted node with its parent, and if the inserted node is larger, we swap them. We continue this process from bottom to top until we move past the root or reach a node that no longer needs to be swapped.

    <1><2><3><4><5><6><7><8><9>

    Figure 8-3   Steps of inserting an element into the heap

    Given a total of \\(n\\) nodes, the tree height is \\(O(\\log n)\\). Thus, the number of loop iterations in the heapify operation is at most \\(O(\\log n)\\), making the time complexity of the element insertion operation \\(O(\\log n)\\). The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def push(self, val: int):\n    \"\"\"Element enters heap\"\"\"\n    # Add node\n    self.max_heap.append(val)\n    # Heapify from bottom to top\n    self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n    \"\"\"Starting from node i, heapify from bottom to top\"\"\"\n    while True:\n        # Get parent node of node i\n        p = self.parent(i)\n        # When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # Swap two nodes\n        self.swap(i, p)\n        # Loop upward heapify\n        i = p\n
    my_heap.cpp
    /* Element enters heap */\nvoid push(int val) {\n    // Add node\n    maxHeap.push_back(val);\n    // Heapify from bottom to top\n    siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Swap two nodes\n        swap(maxHeap[i], maxHeap[p]);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.java
    /* Element enters heap */\nvoid push(int val) {\n    // Add node\n    maxHeap.add(val);\n    // Heapify from bottom to top\n    siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // Swap two nodes\n        swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.cs
    /* Element enters heap */\nvoid Push(int val) {\n    // Add node\n    maxHeap.Add(val);\n    // Heapify from bottom to top\n    SiftUp(Size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid SiftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = Parent(i);\n        // If 'past root node' or 'node needs no repair', end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Swap two nodes\n        Swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.go
    /* Element enters heap */\nfunc (h *maxHeap) push(val any) {\n    // Add node\n    h.data = append(h.data, val)\n    // Heapify from bottom to top\n    h.siftUp(len(h.data) - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfunc (h *maxHeap) siftUp(i int) {\n    for true {\n        // Get parent node of node i\n        p := h.parent(i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // Swap two nodes\n        h.swap(i, p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
    my_heap.swift
    /* Element enters heap */\nfunc push(val: Int) {\n    // Add node\n    maxHeap.append(val)\n    // Heapify from bottom to top\n    siftUp(i: size() - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfunc siftUp(i: Int) {\n    var i = i\n    while true {\n        // Get parent node of node i\n        let p = parent(i: i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // Swap two nodes\n        swap(i: i, j: p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
    my_heap.js
    /* Element enters heap */\npush(val) {\n    // Add node\n    this.#maxHeap.push(val);\n    // Heapify from bottom to top\n    this.#siftUp(this.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\n#siftUp(i) {\n    while (true) {\n        // Get parent node of node i\n        const p = this.#parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // Swap two nodes\n        this.#swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.ts
    /* Element enters heap */\npush(val: number): void {\n    // Add node\n    this.maxHeap.push(val);\n    // Heapify from bottom to top\n    this.siftUp(this.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nsiftUp(i: number): void {\n    while (true) {\n        // Get parent node of node i\n        const p = this.parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // Swap two nodes\n        this.swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.dart
    /* Element enters heap */\nvoid push(int val) {\n  // Add node\n  _maxHeap.add(val);\n  // Heapify from bottom to top\n  siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n  while (true) {\n    // Get parent node of node i\n    int p = _parent(i);\n    // When \"crossing root node\" or \"node needs no repair\", end heapify\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // Swap two nodes\n    _swap(i, p);\n    // Loop upward heapify\n    i = p;\n  }\n}\n
    my_heap.rs
    /* Element enters heap */\nfn push(&mut self, val: i32) {\n    // Add node\n    self.max_heap.push(val);\n    // Heapify from bottom to top\n    self.sift_up(self.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nfn sift_up(&mut self, mut i: usize) {\n    loop {\n        // Node i is already the heap root, end heapification\n        if i == 0 {\n            break;\n        }\n        // Get parent node of node i\n        let p = Self::parent(i);\n        // When \"node needs no repair\", end heapification\n        if self.max_heap[i] <= self.max_heap[p] {\n            break;\n        }\n        // Swap two nodes\n        self.swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.c
    /* Element enters heap */\nvoid push(MaxHeap *maxHeap, int val) {\n    // By default, should not add this many nodes\n    if (maxHeap->size == MAX_SIZE) {\n        printf(\"heap is full!\");\n        return;\n    }\n    // Add node\n    maxHeap->data[maxHeap->size] = val;\n    maxHeap->size++;\n\n    // Heapify from bottom to top\n    siftUp(maxHeap, maxHeap->size - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(maxHeap, i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // Swap two nodes\n        swap(maxHeap, i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.kt
    /* Element enters heap */\nfun push(_val: Int) {\n    // Add node\n    maxHeap.add(_val)\n    // Heapify from bottom to top\n    siftUp(size() - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfun siftUp(it: Int) {\n    // Kotlin function parameters are immutable, so create temporary variable\n    var i = it\n    while (true) {\n        // Get parent node of node i\n        val p = parent(i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // Swap two nodes\n        swap(i, p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
    my_heap.rb
    ### Push element to heap ###\ndef push(val)\n  # Add node\n  @max_heap << val\n  # Heapify from bottom to top\n  sift_up(size - 1)\nend\n\n### Heapify from node i, bottom to top ###\ndef sift_up(i)\n  loop do\n    # Get parent node of node i\n    p = parent(i)\n    # When \"crossing root node\" or \"node needs no repair\", end heapify\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # Swap two nodes\n    swap(i, p)\n    # Loop upward heapify\n    i = p\n  end\nend\n
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#4-removing-the-heap-top-element","level":3,"title":"4.   Removing the Heap Top Element","text":"

    The heap top element is the root node of the binary tree, which is the first element of the list. If we directly remove the first element from the list, all node indexes in the binary tree would change, making subsequent repair with heapify difficult. To minimize changes in element indexes, we use the following steps.

    1. Swap the heap top element with the heap bottom element (swap the root node with the rightmost leaf node).
    2. After swapping, remove the heap bottom from the list (note that since we've swapped, we're actually removing the original heap top element).
    3. Starting from the root node, perform heapify from top to bottom.

    As shown in Figure 8-4, the direction of \"top-to-bottom heapify\" is opposite to \"bottom-to-top heapify\". We compare the root node's value with its two children and swap it with the largest child. Then loop this operation until we pass a leaf node or encounter a node that doesn't need swapping.

    <1><2><3><4><5><6><7><8><9><10>

    Figure 8-4   Steps of removing the heap top element

    Similar to the element insertion operation, the time complexity of the heap top element removal operation is also \\(O(\\log n)\\). The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def pop(self) -> int:\n    \"\"\"Element exits heap\"\"\"\n    # Handle empty case\n    if self.is_empty():\n        raise IndexError(\"Heap is empty\")\n    # Swap root node with rightmost leaf node (swap first element with last element)\n    self.swap(0, self.size() - 1)\n    # Delete node\n    val = self.max_heap.pop()\n    # Heapify from top to bottom\n    self.sift_down(0)\n    # Return top element\n    return val\n\ndef sift_down(self, i: int):\n    \"\"\"Starting from node i, heapify from top to bottom\"\"\"\n    while True:\n        # Find node with largest value among i, l, r, denoted as 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        # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        if ma == i:\n            break\n        # Swap two nodes\n        self.swap(i, ma)\n        # Loop downward heapify\n        i = ma\n
    my_heap.cpp
    /* Element exits heap */\nvoid pop() {\n    // Handle empty case\n    if (isEmpty()) {\n        throw out_of_range(\"Heap is empty\");\n    }\n    // Delete node\n    swap(maxHeap[0], maxHeap[size() - 1]);\n    // Remove node\n    maxHeap.pop_back();\n    // Return top element\n    siftDown(0);\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i)\n            break;\n        swap(maxHeap[i], maxHeap[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.java
    /* Element exits heap */\nint pop() {\n    // Handle empty case\n    if (isEmpty())\n        throw new IndexOutOfBoundsException();\n    // Delete node\n    swap(0, size() - 1);\n    // Remove node\n    int val = maxHeap.remove(size() - 1);\n    // Return top element\n    siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.cs
    /* Element exits heap */\nint Pop() {\n    // Handle empty case\n    if (IsEmpty())\n        throw new IndexOutOfRangeException();\n    // Delete node\n    Swap(0, Size() - 1);\n    // Remove node\n    int val = maxHeap.Last();\n    maxHeap.RemoveAt(Size() - 1);\n    // Return top element\n    SiftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid SiftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // If 'node i is largest' or 'past leaf node', end heapify\n        if (ma == i) break;\n        // Swap two nodes\n        Swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.go
    /* Element exits heap */\nfunc (h *maxHeap) pop() any {\n    // Handle empty case\n    if h.isEmpty() {\n        fmt.Println(\"error\")\n        return nil\n    }\n    // Delete node\n    h.swap(0, h.size()-1)\n    // Remove node\n    val := h.data[len(h.data)-1]\n    h.data = h.data[:len(h.data)-1]\n    // Return top element\n    h.siftDown(0)\n\n    // Return heap top element\n    return val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfunc (h *maxHeap) siftDown(i int) {\n    for true {\n        // Find node with maximum value among nodes i, l, r, denoted as 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        // Swap two nodes\n        if max == i {\n            break\n        }\n        // Swap two nodes\n        h.swap(i, max)\n        // Loop downwards heapification\n        i = max\n    }\n}\n
    my_heap.swift
    /* Element exits heap */\nfunc pop() -> Int {\n    // Handle empty case\n    if isEmpty() {\n        fatalError(\"Heap is empty\")\n    }\n    // Delete node\n    swap(i: 0, j: size() - 1)\n    // Remove node\n    let val = maxHeap.remove(at: size() - 1)\n    // Return top element\n    siftDown(i: 0)\n    // Return heap top element\n    return val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfunc siftDown(i: Int) {\n    var i = i\n    while true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        swap(i: i, j: ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n
    my_heap.js
    /* Element exits heap */\npop() {\n    // Handle empty case\n    if (this.isEmpty()) throw new Error('Heap is empty');\n    // Delete node\n    this.#swap(0, this.size() - 1);\n    // Remove node\n    const val = this.#maxHeap.pop();\n    // Return top element\n    this.#siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\n#siftDown(i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma === i) break;\n        // Swap two nodes\n        this.#swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.ts
    /* Element exits heap */\npop(): number {\n    // Handle empty case\n    if (this.isEmpty()) throw new RangeError('Heap is empty.');\n    // Delete node\n    this.swap(0, this.size() - 1);\n    // Remove node\n    const val = this.maxHeap.pop();\n    // Return top element\n    this.siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nsiftDown(i: number): void {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma === i) break;\n        // Swap two nodes\n        this.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.dart
    /* Element exits heap */\nint pop() {\n  // Handle empty case\n  if (isEmpty()) throw Exception('Heap is empty');\n  // Delete node\n  _swap(0, size() - 1);\n  // Remove node\n  int val = _maxHeap.removeLast();\n  // Return top element\n  siftDown(0);\n  // Return heap top element\n  return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n  while (true) {\n    // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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    // Swap two nodes\n    if (ma == i) break;\n    // Swap two nodes\n    _swap(i, ma);\n    // Loop downwards heapification\n    i = ma;\n  }\n}\n
    my_heap.rs
    /* Element exits heap */\nfn pop(&mut self) -> i32 {\n    // Handle empty case\n    if self.is_empty() {\n        panic!(\"index out of bounds\");\n    }\n    // Delete node\n    self.swap(0, self.size() - 1);\n    // Remove node\n    let val = self.max_heap.pop().unwrap();\n    // Return top element\n    self.sift_down(0);\n    // Return heap top element\n    val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfn sift_down(&mut self, mut i: usize) {\n    loop {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break;\n        }\n        // Swap two nodes\n        self.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.c
    /* Element exits heap */\nint pop(MaxHeap *maxHeap) {\n    // Handle empty case\n    if (isEmpty(maxHeap)) {\n        printf(\"heap is empty!\");\n        return INT_MAX;\n    }\n    // Delete node\n    swap(maxHeap, 0, size(maxHeap) - 1);\n    // Remove node\n    int val = maxHeap->data[maxHeap->size - 1];\n    maxHeap->size--;\n    // Return top element\n    siftDown(maxHeap, 0);\n\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Find node with maximum value among nodes i, l, r, denoted as 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        // Swap two nodes\n        if (max == i) {\n            break;\n        }\n        // Swap two nodes\n        swap(maxHeap, i, max);\n        // Loop downwards heapification\n        i = max;\n    }\n}\n
    my_heap.kt
    /* Element exits heap */\nfun pop(): Int {\n    // Handle empty case\n    if (isEmpty()) throw IndexOutOfBoundsException()\n    // Delete node\n    swap(0, size() - 1)\n    // Remove node\n    val _val = maxHeap.removeAt(size() - 1)\n    // Return top element\n    siftDown(0)\n    // Return heap top element\n    return _val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfun siftDown(it: Int) {\n    // Kotlin function parameters are immutable, so create temporary variable\n    var i = it\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i) break\n        // Swap two nodes\n        swap(i, ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n
    my_heap.rb
    ### Pop element from heap ###\ndef pop\n  # Handle empty case\n  raise IndexError, \"Heap is empty\" if is_empty?\n  # Delete node\n  swap(0, size - 1)\n  # Remove node\n  val = @max_heap.pop\n  # Return top element\n  sift_down(0)\n  # Return heap top element\n  val\nend\n\n### Heapify from node i, top to bottom ###\ndef sift_down(i)\n  loop do\n    # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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    # Swap two nodes\n    break if ma == i\n\n    # Swap two nodes\n    swap(i, ma)\n    # Loop downwards heapification\n    i = ma\n  end\nend\n
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#813-common-applications-of-heaps","level":2,"title":"8.1.3   Common Applications of Heaps","text":"
    • Priority queue: Heaps are typically the preferred data structure for implementing priority queues. The time complexity of both enqueue and dequeue operations is \\(O(\\log n)\\), and heap construction has a time complexity of \\(O(n)\\), making these operations highly efficient.
    • Heap sort: Given a set of data, we can build a heap with them and then continuously perform element removal operations to obtain sorted data. However, we usually use a more elegant approach to implement heap sort, as detailed in the \"Heap Sort\" chapter.
    • Getting the largest \\(k\\) elements: This is a classic algorithm problem and also a typical application, such as selecting the top 10 trending news items for Weibo Hot Search or the top 10 best-selling products.
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   Summary","text":"","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • A heap is a complete binary tree. Depending on the property it satisfies, it can be classified as either a max heap or a min heap. The top element of a max heap (min heap) is the largest (smallest) element.
    • A priority queue is a queue in which elements are dequeued according to priority, and it is typically implemented using a heap.
    • Common heap operations and their corresponding time complexities include inserting an element \\(O(\\log n)\\), removing the top element \\(O(\\log n)\\), and accessing the top element \\(O(1)\\).
    • Complete binary trees are well-suited for array representation, so we typically use arrays to store heaps.
    • Heapify operations are used to maintain the heap property and are employed in both element insertion and removal operations.
    • Building a heap from \\(n\\) input elements can be optimized to \\(O(n)\\), which is highly efficient.
    • Top-k is a classic algorithmic problem that can be solved efficiently using a heap, with a time complexity of \\(O(n \\log k)\\).
    ","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Does the term \"heap\" in data structures mean the same thing as \"heap\" in memory management?

    They are not the same concept; they simply share the same name. In computer systems, the heap is part of dynamic memory allocation, and programs can use it to store data at runtime. A program can request a certain amount of heap memory to store complex structures such as objects and arrays. When the data is no longer needed, the program must release that memory to prevent memory leaks. Compared with stack memory, heap memory requires more careful management and use; improper handling can lead to problems such as memory leaks and dangling pointers.

    ","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Top-k Problem","text":"

    Question

    Given an unordered array nums of length \\(n\\), return the largest \\(k\\) elements in the array.

    For this problem, we will first introduce two relatively straightforward solutions, followed by a more efficient heap-based solution.

    ","path":["Chapter 8. Heap","8.3   Top-k Problem"],"tags":[]},{"location":"chapter_heap/top_k/#831-method-1-iterative-selection","level":2,"title":"8.3.1   Method 1: Iterative Selection","text":"

    We can perform \\(k\\) rounds of traversal as shown in Figure 8-6, extracting the \\(1^{st}\\), \\(2^{nd}\\), \\(\\dots\\), \\(k^{th}\\) largest elements in each round, with a time complexity of \\(O(nk)\\).

    This method is only suitable when \\(k \\ll n\\), because when \\(k\\) is close to \\(n\\), the time complexity approaches \\(O(n^2)\\), making it very inefficient.

    Figure 8-6   Traversing to find the largest k elements

    Tip

    When \\(k = n\\), we can obtain a complete sorted sequence, which is equivalent to the \"selection sort\" algorithm.

    ","path":["Chapter 8. Heap","8.3   Top-k Problem"],"tags":[]},{"location":"chapter_heap/top_k/#832-method-2-sorting","level":2,"title":"8.3.2   Method 2: Sorting","text":"

    As shown in Figure 8-7, we can first sort the array nums, then return the rightmost \\(k\\) elements, with a time complexity of \\(O(n \\log n)\\).

    Clearly, this method does more work than necessary, because we only need to find the largest \\(k\\) elements rather than sort the other elements.

    Figure 8-7   Sorting to find the largest k elements

    ","path":["Chapter 8. Heap","8.3   Top-k Problem"],"tags":[]},{"location":"chapter_heap/top_k/#833-method-3-heap","level":2,"title":"8.3.3   Method 3: Heap","text":"

    We can solve the Top-k problem more efficiently with a heap, as shown in Figure 8-8.

    1. Initialize a min heap, where the heap top element is the smallest.
    2. First, insert the first \\(k\\) elements of the array into the heap in sequence.
    3. Starting from the \\((k + 1)^{th}\\) element, if the current element is greater than the heap top element, remove the heap top element and insert the current element into the heap.
    4. After traversal is complete, the heap contains the largest \\(k\\) elements.
    <1><2><3><4><5><6><7><8><9>

    Figure 8-8   Finding the largest k elements using a heap

    Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py
    def top_k_heap(nums: list[int], k: int) -> list[int]:\n    \"\"\"Find the largest k elements in array based on heap\"\"\"\n    # Initialize min heap\n    heap = []\n    # Enter the first k elements of array into heap\n    for i in range(k):\n        heapq.heappush(heap, nums[i])\n    # Starting from the (k+1)th element, maintain heap length as k\n    for i in range(k, len(nums)):\n        # If current element is greater than top element, top element exits heap, current element enters heap\n        if nums[i] > heap[0]:\n            heapq.heappop(heap)\n            heapq.heappush(heap, nums[i])\n    return heap\n
    top_k.cpp
    /* Find the largest k elements in array based on heap */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n    // Python's heapq module implements min heap by default\n    priority_queue<int, vector<int>, greater<int>> heap;\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.push(nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.size(); i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.top()) {\n            heap.pop();\n            heap.push(nums[i]);\n        }\n    }\n    return heap;\n}\n
    top_k.java
    /* Find the largest k elements in array based on heap */\nQueue<Integer> topKHeap(int[] nums, int k) {\n    // Python's heapq module implements min heap by default\n    Queue<Integer> heap = new PriorityQueue<Integer>();\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.offer(nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.peek()) {\n            heap.poll();\n            heap.offer(nums[i]);\n        }\n    }\n    return heap;\n}\n
    top_k.cs
    /* Find the largest k elements in array based on heap */\nPriorityQueue<int, int> TopKHeap(int[] nums, int k) {\n    // Python's heapq module implements min heap by default\n    PriorityQueue<int, int> heap = new();\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.Enqueue(nums[i], nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.Length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\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
    /* Find the largest k elements in array based on heap */\nfunc topKHeap(nums []int, k int) *minHeap {\n    // Python's heapq module implements min heap by default\n    h := &minHeap{}\n    heap.Init(h)\n    // Enter the first k elements of array into heap\n    for i := 0; i < k; i++ {\n        heap.Push(h, nums[i])\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for i := k; i < len(nums); i++ {\n        // If current element is greater than top element, top element exits heap, current element enters heap\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
    /* Find the largest k elements in array based on heap */\nfunc topKHeap(nums: [Int], k: Int) -> [Int] {\n    // Initialize min heap and build heap with first k elements\n    var heap = Heap(nums.prefix(k))\n    // Starting from the (k+1)th element, maintain heap length as k\n    for i in nums.indices.dropFirst(k) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\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
    /* Element enters heap */\nfunction pushMinHeap(maxHeap, val) {\n    // Negate element\n    maxHeap.push(-val);\n}\n\n/* Element exits heap */\nfunction popMinHeap(maxHeap) {\n    // Negate element\n    return -maxHeap.pop();\n}\n\n/* Access top element */\nfunction peekMinHeap(maxHeap) {\n    // Negate element\n    return -maxHeap.peek();\n}\n\n/* Extract elements from heap */\nfunction getMinHeap(maxHeap) {\n    // Negate element\n    return maxHeap.getMaxHeap().map((num) => -num);\n}\n\n/* Find the largest k elements in array based on heap */\nfunction topKHeap(nums, k) {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    const maxHeap = new MaxHeap([]);\n    // Enter the first k elements of array into heap\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (let i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Return elements in heap\n    return getMinHeap(maxHeap);\n}\n
    top_k.ts
    /* Element enters heap */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n    // Negate element\n    maxHeap.push(-val);\n}\n\n/* Element exits heap */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n    // Negate element\n    return -maxHeap.pop();\n}\n\n/* Access top element */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n    // Negate element\n    return -maxHeap.peek();\n}\n\n/* Extract elements from heap */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n    // Negate element\n    return maxHeap.getMaxHeap().map((num: number) => -num);\n}\n\n/* Find the largest k elements in array based on heap */\nfunction topKHeap(nums: number[], k: number): number[] {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    const maxHeap = new MaxHeap([]);\n    // Enter the first k elements of array into heap\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (let i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Return elements in heap\n    return getMinHeap(maxHeap);\n}\n
    top_k.dart
    /* Find the largest k elements in array based on heap */\nMinHeap topKHeap(List<int> nums, int k) {\n  // Initialize min heap, push first k elements of array to heap\n  MinHeap heap = MinHeap(nums.sublist(0, k));\n  // Starting from the (k+1)th element, maintain heap length as k\n  for (int i = k; i < nums.length; i++) {\n    // If current element is greater than top element, top element exits heap, current element enters heap\n    if (nums[i] > heap.peek()) {\n      heap.pop();\n      heap.push(nums[i]);\n    }\n  }\n  return heap;\n}\n
    top_k.rs
    /* Find the largest k elements in array based on heap */\nfn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {\n    // BinaryHeap is a max heap, use Reverse to negate elements to implement min heap\n    let mut heap = BinaryHeap::<Reverse<i32>>::new();\n    // Enter the first k elements of array into heap\n    for &num in nums.iter().take(k) {\n        heap.push(Reverse(num));\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for &num in nums.iter().skip(k) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if num > heap.peek().unwrap().0 {\n            heap.pop();\n            heap.push(Reverse(num));\n        }\n    }\n    heap\n}\n
    top_k.c
    /* Element enters heap */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n    // Negate element\n    push(maxHeap, -val);\n}\n\n/* Element exits heap */\nint popMinHeap(MaxHeap *maxHeap) {\n    // Negate element\n    return -pop(maxHeap);\n}\n\n/* Access top element */\nint peekMinHeap(MaxHeap *maxHeap) {\n    // Negate element\n    return -peek(maxHeap);\n}\n\n/* Extract elements from heap */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Negate all heap elements and store in res array\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/* Extract elements from heap */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Negate all heap elements and store in res array\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// Function to find k largest elements in array using heap\nint *topKHeap(int *nums, int sizeNums, int k) {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    int *empty = (int *)malloc(0);\n    MaxHeap *maxHeap = newMaxHeap(empty, 0);\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < sizeNums; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    int *res = getMinHeap(maxHeap);\n    // Free memory\n    delMaxHeap(maxHeap);\n    return res;\n}\n
    top_k.kt
    /* Find the largest k elements in array based on heap */\nfun topKHeap(nums: IntArray, k: Int): Queue<Int> {\n    // Python's heapq module implements min heap by default\n    val heap = PriorityQueue<Int>()\n    // Enter the first k elements of array into heap\n    for (i in 0..<k) {\n        heap.offer(nums[i])\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (i in k..<nums.size) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.peek()) {\n            heap.poll()\n            heap.offer(nums[i])\n        }\n    }\n    return heap\n}\n
    top_k.rb
    ### Find largest k elements in array using heap ###\ndef top_k_heap(nums, k)\n  # Python's heapq module implements min heap by default\n  # Note: We negate all heap elements to simulate min heap using max heap\n  max_heap = MaxHeap.new([])\n\n  # Enter the first k elements of array into heap\n  for i in 0...k\n    push_min_heap(max_heap, nums[i])\n  end\n\n  # Starting from the (k+1)th element, maintain heap length as k\n  for i in k...nums.length\n    # If current element is greater than top element, top element exits heap, current element enters heap\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

    A total of \\(n\\) rounds of heap insertions and removals are performed, with the heap's maximum length being \\(k\\), so the time complexity is \\(O(n \\log k)\\). This method is very efficient; when \\(k\\) is small, the time complexity approaches \\(O(n)\\); when \\(k\\) is large, the time complexity does not exceed \\(O(n \\log n)\\).

    Additionally, this method is well suited to dynamic data streams. As new data arrives, we can continuously maintain the elements in the heap, enabling dynamic updates to the largest \\(k\\) elements.

    ","path":["Chapter 8. Heap","8.3   Top-k Problem"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"Preface","text":"

    A few years ago, I shared the \"Sword for Offer\" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most frequently asked question I encountered was \"how to get started with algorithms.\" Gradually, I developed a keen interest in this question.

    Diving straight into problem-solving seems to be the most popular approach—it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-learning abilities can successfully defuse the mines one by one, while those with insufficient foundations may end up bruised and battered, retreating step by step in frustration. Reading through textbooks is also a common practice, but for job seekers, graduation theses, resume submissions, and preparations for written tests and interviews have already consumed most of their energy, making working through thick books an arduous challenge.

    If you're facing similar struggles, then it's fortunate that this book has \"found\" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you through the \"landscape\" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different \"mines,\" and enable you to master various \"mine-clearing methods.\" With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system.

    I deeply agree with Professor Feynman's words: \"Knowledge isn't free. You have to pay attention.\" In this sense, this book is not entirely \"free.\" In order to live up to the precious \"attention\" you invest in this book, I will do my utmost and devote my greatest \"attention\" to completing this work.

    I'm keenly aware of the limits of my knowledge and experience. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students.

    Hello, Algorithms!

    The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all striking demonstrations of algorithms at work on computers.

    In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms gradually became more refined and complex. From the ingenious craftsmanship of master artisans, to industrial products that liberate productive forces, to the scientific laws governing the operation of the universe, behind almost every ordinary or astonishing thing lies ingenious algorithmic thinking.

    Similarly, data structures are everywhere: from large-scale social networks to small subway systems, many systems can be modeled as \"graphs\"; from a nation to a family, the primary organizational forms of society exhibit characteristics of \"trees\"; winter clothing is like a \"stack,\" where the first item put on is the last to be taken off; a badminton tube is like a \"queue,\" with items inserted at one end and retrieved from the other; a dictionary is like a \"hash table,\" enabling quick lookup of target entries.

    This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them in code. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you!

    ","path":["Before Starting","Preface"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"Chapter 1.   Encounter with Algorithms","text":"

    Abstract

    A young girl dances gracefully, intertwined with data, her skirt flowing with the melody of algorithms.

    She invites you to dance with her. Follow her steps closely and enter the world of algorithms, full of logic and beauty.

    ","path":["Chapter 1. Encounter with Algorithms","Chapter 1.   Encounter with Algorithms"],"tags":[]},{"location":"chapter_introduction/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 1.1   Algorithms Are Everywhere
    • 1.2   What Is an Algorithm
    • 1.3   Summary
    ","path":["Chapter 1. Encounter with Algorithms","Chapter 1.   Encounter with Algorithms"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   Algorithms Are Everywhere","text":"

    When we hear the term \"algorithm,\" we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives.

    Before we formally explore algorithms, here's an interesting fact worth sharing: you have already learned many algorithms without realizing it, and you are used to applying them in daily life. Let me give a few specific examples to illustrate this point.

    Example 1: Looking Up a Dictionary. In an English dictionary, words are listed alphabetically. Assuming we're searching for a word that starts with the letter \\(r\\), this is typically done in the following way:

    1. Open the dictionary to about halfway and check the first word on that page; suppose it starts with the letter \\(m\\).
    2. Since \\(r\\) comes after \\(m\\) in the alphabet, the first half can be ignored and the search space is narrowed down to the second half.
    3. Repeat steps 1. and 2. until you find the page where the word starts with \\(r\\).
    <1><2><3><4><5>

    Figure 1-1   Process of looking up a dictionary

    Looking up a dictionary, an essential skill for elementary school students is actually the famous \"Binary Search\" algorithm. From a data structure perspective, we can consider the dictionary as a sorted \"array\"; from an algorithmic perspective, the series of actions taken to look up a word in the dictionary can be viewed as the algorithm \"Binary Search.\"

    Example 2: Organizing Playing Cards. When playing cards, we need to arrange the cards in our hands in ascending order, as shown in the following process.

    1. Divide the playing cards into \"ordered\" and \"unordered\" sections, assuming initially the leftmost card is already in order.
    2. Take out a card from the unordered section and insert it into the correct position in the ordered section; after this, the leftmost two cards are in order.
    3. Repeat step 2 until all cards are in order.

    Figure 1-2   Process of sorting a deck of cards

    The above method of organizing playing cards is essentially the \"Insertion Sort\" algorithm, which is very efficient for small datasets. Many programming languages' built-in sorting implementations use insertion sort internally.

    Example 3: Making Change. Assume making a purchase of \\(69\\) at a supermarket. If you give the cashier \\(100\\), they will need to provide you with \\(31\\) in change. This process can be clearly understood as illustrated in Figure 1-3.

    1. The available denominations smaller than \\(31\\) are \\(1\\), \\(5\\), \\(10\\), and \\(20\\).
    2. Take out the largest \\(20\\) from the options, leaving \\(31 - 20 = 11\\).
    3. Take out the largest \\(10\\) from the remaining options, leaving \\(11 - 10 = 1\\).
    4. Take out the largest \\(1\\) from the remaining options, leaving \\(1 - 1 = 0\\).
    5. Complete change-making, the solution is \\(20 + 10 + 1 = 31\\).

    Figure 1-3   Process of making change

    In the steps above, we choose what seems to be the best option at each stage by using the largest denomination available, which leads to an effective way to make change. From a data structures and algorithms perspective, this approach is known as a \"Greedy\" algorithm.

    From cooking a meal to interstellar travel, almost all problem-solving involves algorithms. The advent of computers allows us to store data structures in memory and write code to call the CPU and GPU to execute algorithms. In this way, we can transfer real-life problems to computers and solve various complex issues in a more efficient way.

    Tip

    If concepts such as data structures, algorithms, arrays, and binary search still feel only half-familiar, keep reading. This book will guide you into the world of data structures and algorithms.

    ","path":["Chapter 1. Encounter with Algorithms","1.1   Algorithms Are Everywhere"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   Summary","text":"","path":["Chapter 1. Encounter with Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Algorithms are ubiquitous in daily life and are not some distant, esoteric body of knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life.
    • The principle of looking up a dictionary is consistent with the binary search algorithm. Binary search embodies the important algorithmic idea of divide and conquer.
    • The process of organizing playing cards is very similar to the insertion sort algorithm. Insertion sort is suitable for sorting small datasets.
    • The steps of making change are essentially a greedy algorithm, where the best choice is made at each step based on the current situation.
    • An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is a way of organizing and storing data in a computer.
    • Data structures and algorithms are closely connected. Data structures are the foundation of algorithms, and algorithms breathe life into data structures.
    • We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the way they are shaped and connected represents the data structure, and the steps used to assemble them correspond to the algorithm.
    ","path":["Chapter 1. Encounter with Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: As a programmer, I have never used algorithms to solve problems in my daily work. Common algorithms are already encapsulated by programming languages and can be used directly. Does this mean that the problems in our work have not yet reached the level where algorithms are needed?

    If we compare specific work skills to \"techniques\" in martial arts, then fundamental subjects should be more like \"internal skills\".

    I believe the significance of learning algorithms (and other fundamental subjects) is not that you will need to implement them from scratch at work, but that the knowledge you gain enables you to make sound professional judgments when solving problems, thereby improving the overall quality of your work. Here is a simple example. Every programming language has a built-in sorting function:

    • If we have not studied data structures and algorithms, we might simply feed any given data to this sorting function. It runs smoothly with good performance, and there doesn't seem to be any problem.
    • But if we have studied algorithms, we would know that the time complexity of the built-in sorting function is \\(O(n \\log n)\\). However, if the given data consists of integers with a fixed number of digits (such as student IDs), we can use the more efficient \"radix sort\", reducing the time complexity to \\(O(nk)\\), where \\(k\\) is the number of digits. When the data volume is very large, the saved running time can create significant value (reduced costs, improved experience, etc.).

    In engineering, many problems are difficult to solve optimally, and many others are only solved \"well enough.\" The difficulty of a problem depends, on the one hand, on the nature of the problem itself and, on the other hand, on the knowledge of the person examining it. The more complete a person's knowledge and the more experience they have, the deeper their analysis will be, and the more elegantly the problem can be solved.

    ","path":["Chapter 1. Encounter with Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   What Is an Algorithm","text":"","path":["Chapter 1. Encounter with Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121-algorithm-definition","level":2,"title":"1.2.1   Algorithm Definition","text":"

    An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time. It has the following characteristics.

    • The problem is well-defined, with clear input and output definitions.
    • It is feasible and can be completed with finite steps, time, and memory.
    • Each step has a definite meaning, and under the same input and operating conditions, the output is always the same.
    ","path":["Chapter 1. Encounter with Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122-data-structure-definition","level":2,"title":"1.2.2   Data Structure Definition","text":"

    A data structure is a way of organizing and storing data, including the data itself, the relationships between data elements, and the methods used to operate on them. It has the following design objectives.

    • Occupy as little space as possible to save computer memory.
    • Data operations should be as fast as possible, covering data access, addition, deletion, update, etc.
    • Provide a concise data representation and logical information so that algorithms can run efficiently.

    Data structure design is a process full of trade-offs. If we want to achieve improvements in one aspect, we often need to make compromises in another aspect. Here are two examples.

    • Compared to arrays, linked lists are more convenient for data addition and deletion operations but sacrifice data access speed.
    • Compared to linked lists, graphs provide richer logical information but require larger memory space.
    ","path":["Chapter 1. Encounter with Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123-the-relationship-between-data-structures-and-algorithms","level":2,"title":"1.2.3   The Relationship Between Data Structures and Algorithms","text":"

    As shown in Figure 1-4, data structures and algorithms are highly related and tightly coupled, specifically manifested in the following three aspects.

    • Data structures are the foundation of algorithms. Data structures provide algorithms with structured storage of data and methods for operating on data.
    • Algorithms breathe life into data structures. Data structures themselves only store data information; combined with algorithms, they can solve specific problems.
    • Algorithms can usually be implemented based on different data structures, but execution efficiency may vary greatly. Choosing the appropriate data structure is key.

    Figure 1-4   The relationship between data structures and algorithms

    Data structures and algorithms are like assembling building blocks as shown in Figure 1-5. A set of building blocks, in addition to containing many parts, also comes with detailed assembly instructions. By following the instructions step by step, we can assemble an exquisite building block model.

    Figure 1-5   Assembling blocks

    The detailed correspondence between the two is shown in Table 1-1.

    Table 1-1   Comparing data structures and algorithms to assembling building blocks

    Data structures and algorithms Assembling building blocks Input data Unassembled building blocks Data structure Organization form of building blocks, including shape, size, connection method, etc. Algorithm A series of operational steps to assemble the blocks into the target form Output data Building block model

    It is worth noting that data structures and algorithms are independent of programming languages. That is why this book can provide implementations in multiple programming languages.

    Conventional abbreviation

    In actual discussions, we usually abbreviate \"data structures and algorithms\" as \"algorithms\". For example, the well-known LeetCode algorithm problems actually examine knowledge of both data structures and algorithms.

    ","path":["Chapter 1. Encounter with Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"Chapter 0.   Preface","text":"

    Abstract

    Algorithms are like a beautiful symphony, each line of code flows like a melody.

    May this book gently resonate in your mind, leaving a unique and profound melody.

    ","path":["Chapter 0. Preface","Chapter 0.   Preface"],"tags":[]},{"location":"chapter_preface/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 0.1   About This Book
    • 0.2   How to Use This Book
    • 0.3   Summary
    ","path":["Chapter 0. Preface","Chapter 0.   Preface"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   About This Book","text":"

    This project aims to create an open-source, free, beginner-friendly introductory tutorial on data structures and algorithms.

    • The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners through the landscape of data structures and algorithms.
    • The source code can be run with one click, helping readers improve their programming skills through practice and understand how algorithms work and the underlying implementation of data structures.
    • We encourage readers to learn from each other, and everyone is welcome to ask questions and share insights in the comments section, making progress together through discussion and exchange.
    ","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#011-target-audience","level":2,"title":"0.1.1   Target Audience","text":"

    If you are an algorithm beginner who has never studied algorithms, or if you already have some problem-solving experience but only a hazy understanding of data structures and algorithms, then this book is tailor-made for you!

    If you have already accumulated a certain amount of problem-solving experience and are familiar with most question types, this book can help you review and organize your algorithm knowledge system, and the repository's source code can be used as a \"problem-solving toolkit\" or \"algorithm dictionary.\"

    If you are an algorithm \"expert,\" we look forward to receiving your valuable suggestions, or joining us as a contributor.

    Prerequisites

    You need basic programming knowledge in at least one language and the ability to read and write simple code.

    ","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#012-content-structure","level":2,"title":"0.1.2   Content Structure","text":"

    The main content of this book is shown in Figure 0-1.

    • Complexity analysis: Evaluation dimensions and methods for data structures and algorithms. Methods for calculating time complexity and space complexity, common types, examples, etc.
    • Data structures: Classification methods for basic data types and data structures. Definitions, advantages and disadvantages, common operations, common types, typical applications, implementation methods, and more for data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs.
    • Algorithms: The definition, advantages and disadvantages, efficiency, application scenarios, problem-solving steps, and example problems of algorithms such as searching, sorting, divide and conquer, backtracking, dynamic programming, and greedy algorithms.

    Figure 0-1   Main content of this book

    ","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#013-acknowledgements","level":2,"title":"0.1.3   Acknowledgements","text":"

    This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every contributor who invested time and effort, they are (in the order automatically generated by 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, and KeiichiKasai.

    The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in; they helped keep the code consistent and standardized across the different language versions.

    The English version of this book was reviewed by yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn and thomasq0; the Japanese version was reviewed by eltociear; the Russian version was reviewed by И. А. Шевкун and Yuyan Huang; and the Traditional Chinese version was reviewed by Shyam-Chen and Dr-XYZ. Thanks to their contributions, this book is able to serve a broader readership, and we are deeply grateful to them.

    The ePub ebook generation tool for this book was developed by zhongfq. We thank him for his contribution, which provides readers with a more flexible way to read.

    During the creation of this book, I received help from many people.

    • Thanks to my mentor at the company, Dr. Li Xi, who encouraged me to \"take action quickly\" during a conversation, strengthening my determination to write this book;
    • Thanks to my girlfriend Bubble, the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more approachable for beginners;
    • Thanks to Tengbao, Qibao, and Feibao for coming up with a creative name for this book, evoking everyone's fond memories of writing their first line of code \"Hello World!\";
    • Thanks to Xiaoquan for providing professional help in intellectual property rights, which played an important role in the improvement of this open-source book;
    • Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently revising them many times at my perfectionist insistence;
    • Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme Material-for-MkDocs.

    During the writing process, I read many textbooks and articles on data structures and algorithms. These works served as excellent models for this book and helped ensure the accuracy and quality of its content. I would like to thank all the teachers and predecessors for their outstanding contributions!

    This book advocates a hands-on approach to learning, and in this respect I was deeply inspired by Dive into Deep Learning. I highly recommend this excellent work to all readers.

    Heartfelt thanks to my parents. It is your support and encouragement that gave me the opportunity to pursue this enjoyable project.

    ","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   How to Use This Book","text":"

    Tip

    For the best reading experience, it is recommended that you read through this section.

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#021-writing-style-conventions","level":2,"title":"0.2.1   Writing Style Conventions","text":"
    • Sections marked with * after the title are optional and somewhat more challenging. If you're short on time, you can skip them on your first pass.
    • Technical terms are shown in bold (in the print and PDF editions) or underlined (in the web edition), such as array. They are worth remembering, as they will help when reading technical literature.
    • Key content and summary statements will be bolded, and such text deserves special attention.
    • Words and phrases with specific meanings will be marked with \"quotation marks\" to avoid ambiguity.
    • When terminology differs across programming languages, this book follows Python conventions; for example, it uses None to represent \"null\".
    • This book partially relaxes conventional programming-language comment styles in favor of a more compact layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    \"\"\"Title comment, used to label functions, classes, test cases, etc.\"\"\"\n\n# Content comment, used to explain code in detail\n\n\"\"\"\nMulti-line\ncomment\n\"\"\"\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n// Multi-line\n// comment\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    ### Title comment, used to label functions, classes, test cases, etc. ###\n\n# Content comment, used to explain code in detail\n\n# Multi-line\n# comment\n
    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#022-learning-efficiently-with-animated-illustrations","level":2,"title":"0.2.2   Learning Efficiently with Animated Illustrations","text":"

    Compared with plain text, videos and images have higher information density and a clearer structure, making them easier to understand. In this book, key concepts and challenging topics are presented mainly through animated illustrations, with text serving as explanation and supplement.

    If, while reading this book, you encounter an animated illustration like the one shown below, treat the illustration as primary and the text as supplementary, and use both together to understand the content.

    Figure 0-2   Example of animated illustrations

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#023-deepening-understanding-through-code-practice","level":2,"title":"0.2.3   Deepening Understanding Through Code Practice","text":"

    The accompanying code for this book is hosted in the GitHub repository. As shown in Figure 0-3, the source code comes with test cases and can be run with one click.

    If time permits, it is recommended that you type out the code yourself. If you have limited study time, please at least read through and run all the code.

    Compared with simply reading code, writing it yourself often brings greater rewards. Hands-on practice is where real learning happens.

    Figure 0-3   Example of running code

    Getting the code running mainly involves three preliminary steps.

    Step 1: Install the local programming environment. Please follow the tutorial in the appendix. If it is already installed, you can skip this step.

    Step 2: Clone or download the code repository. Visit the GitHub repository. If you have already installed Git, you can clone this repository with the following command:

    git clone https://github.com/krahets/hello-algo.git\n

    Alternatively, you can click the \"Download ZIP\" button shown below to download a ZIP archive of the repository directly and then extract it locally.

    Figure 0-4   Clone repository and download code

    Step 3: Run the source code. As shown in Figure 0-5, for code blocks with file names at the top, we can find the corresponding source code files in the codes folder of the repository. The source code files can be run with one click, which will help you save unnecessary debugging time and allow you to focus on learning content.

    Figure 0-5   Code blocks and corresponding source code files

    In addition to running code locally, the web version also supports visual execution of Python code (implemented based on pythontutor). As shown in Figure 0-6, you can click \"Visual Run\" below the code block to expand the view and observe the execution process of the algorithm code; you can also click \"Full Screen View\" for a better viewing experience.

    Figure 0-6   Visual running of Python code

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#024-growing-together-through-questions-and-discussions","level":2,"title":"0.2.4   Growing Together Through Questions and Discussions","text":"

    When reading this book, please do not skip over points that you still do not fully understand. Feel free to ask your questions in the comments section, and my friends and I will do our best to answer them, usually within two days.

    As shown in Figure 0-7, the web version has a comments section at the bottom of each chapter. I encourage you to pay close attention to the discussions there. On the one hand, you can learn about the problems that others encounter, thereby filling gaps in your own understanding and prompting deeper thought. On the other hand, I hope you will generously answer other readers' questions, share your insights, and help others improve.

    Figure 0-7   Example of comments section

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#025-algorithm-learning-roadmap","level":2,"title":"0.2.5   Algorithm Learning Roadmap","text":"

    Overall, we can divide the process of learning data structures and algorithms into three stages.

    1. Stage 1: Algorithm introduction. We need to familiarize ourselves with the characteristics and usage of various data structures, and learn the principles, processes, uses, and efficiency of different algorithms.
    2. Stage 2: Practice algorithm problems. It is recommended to start with popular problems and solve at least 100 of them first, so that you become familiar with mainstream algorithm questions. When you first begin practicing problems, \"knowledge forgetting\" may feel like a challenge, but rest assured, this is very normal. We can review problems according to the \"Ebbinghaus forgetting curve\", and after 3-5 rounds of repetition, they usually stick firmly in memory. For recommended problem lists and practice plans, please see this GitHub repository.
    3. Stage 3: Building a knowledge system. In terms of learning, we can read algorithm column articles, problem-solving frameworks, and algorithm textbooks to continuously enrich our knowledge system. In terms of practicing problems, we can try advanced problem-solving strategies, such as categorization by topic, one problem multiple solutions, one solution multiple problems, etc. Related problem-solving insights can be found in various communities.

    As shown in Figure 0-8, the content of this book mainly covers \"Stage 1\", aiming to help you more efficiently carry out Stage 2 and Stage 3 learning.

    Figure 0-8   Algorithm learning roadmap

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   Summary","text":"","path":["Chapter 0. Preface","0.3   Summary"],"tags":[]},{"location":"chapter_preface/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • The main audience of this book is algorithm beginners. If you already have some background, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a \"problem-solving toolkit.\"
    • The content of the book mainly includes three parts: complexity analysis, data structures, and algorithms, covering most topics in this field.
    • For algorithm novices, reading an introductory book during the initial learning stage is crucial, as it can help you avoid many detours.
    • The animated illustrations in the book are usually used to introduce key concepts and challenging topics. When reading this book, you should pay more attention to these topics.
    • Practice is the best way to learn programming. It is strongly recommended to run the source code and type the code yourself.
    • The web version of this book has a comments section for each chapter, where you are welcome to share your questions and insights at any time.
    ","path":["Chapter 0. Preface","0.3   Summary"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"References","text":"

    [1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).

    [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

    [3] Robert Sedgewick, et al. Algorithms (4th Edition).

    [4] Yan Weimin. Data Structures (C Language Version).

    [5] Deng Junhui. Data Structures (C++ Language Version, Third Edition).

    [6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition).

    [7] Cheng Jie. Data Structures in Plain Language.

    [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, et al. Dive into Deep Learning.

    ","path":["References"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"Chapter 10.   Searching","text":"

    Abstract

    Searching is an adventure into the unknown, where we may need to traverse every corner of the mysterious space, or we may be able to quickly lock onto the target.

    In this journey of discovery, each exploration may yield an unexpected answer.

    ","path":["Chapter 10. Searching","Chapter 10.   Searching"],"tags":[]},{"location":"chapter_searching/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 10.1   Binary Search
    • 10.2   Binary Search Insertion Point
    • 10.3   Binary Search Boundaries
    • 10.4   Hash Optimization Strategy
    • 10.5   Searching Algorithms Revisited
    • 10.6   Summary
    ","path":["Chapter 10. Searching","Chapter 10.   Searching"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   Binary Search","text":"

    Binary search is an efficient search algorithm based on the divide-and-conquer strategy. It leverages the sorted order of the data to reduce the search range by half in each round until the target element is found or the search interval becomes empty.

    Question

    Given an array nums of length \\(n\\) with elements arranged in ascending order and no duplicates, search for and return the index of element target in the array. If the array does not contain the element, return \\(-1\\). An example is shown in Figure 10-1.

    Figure 10-1   Binary search example data

    As shown in Figure 10-2, we first initialize pointers \\(i = 0\\) and \\(j = n - 1\\), pointing to the first and last elements of the array respectively, representing the search interval \\([0, n - 1]\\). Note that square brackets denote a closed interval, which includes the boundary values themselves.

    Next, perform the following two steps in a loop:

    1. Calculate the midpoint index \\(m = \\lfloor {(i + j) / 2} \\rfloor\\), where \\(\\lfloor \\: \\rfloor\\) denotes the floor operation.
    2. Compare nums[m] and target, which results in three cases:
      1. When nums[m] < target, it indicates that target is in the interval \\([m + 1, j]\\), so execute \\(i = m + 1\\).
      2. When nums[m] > target, it indicates that target is in the interval \\([i, m - 1]\\), so execute \\(j = m - 1\\).
      3. When nums[m] = target, it indicates that target has been found, so return index \\(m\\).

    If the array does not contain the target element, the search interval will eventually become empty. In this case, return \\(-1\\).

    <1><2><3><4><5><6><7>

    Figure 10-2   Binary search process

    It's worth noting that since both \\(i\\) and \\(j\\) are of int type, \\(i + j\\) may exceed the range of the int type. To avoid integer overflow, we typically use the formula \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) to calculate the midpoint.

    The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
    def binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Binary search (closed interval)\"\"\"\n    # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    i, j = 0, len(nums) - 1\n    # Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j:\n        # In theory, Python numbers can be infinitely large (depending on memory size), no need to consider large number overflow\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # This means target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # This means target is in the interval [i, m-1]\n        else:\n            return m  # Found the target element, return its index\n    return -1  # Target element not found, return -1\n
    binary_search.cpp
    /* Binary search (closed interval on both sides) */\nint binarySearch(vector<int> &nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.size() - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.java
    /* Binary search (closed interval on both sides) */\nint binarySearch(int[] nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.cs
    /* Binary search (closed interval on both sides) */\nint BinarySearch(int[] nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.Length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2;   // Calculate the midpoint index m\n        if (nums[m] < target)      // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else                       // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.go
    /* Binary search (closed interval on both sides) */\nfunc binarySearch(nums []int, target int) int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    i, j := 0, len(nums)-1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    for i <= j {\n        m := i + (j-i)/2      // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m-1]\n            j = m - 1\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.swift
    /* Binary search (closed interval on both sides) */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m-1]\n            j = m - 1\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.js
    /* Binary search (closed interval on both sides) */\nfunction binarySearch(nums, target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let i = 0,\n        j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        // Calculate midpoint index m, use parseInt() to round down\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target)\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else return m; // Found the target element, return its index\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.ts
    /* Binary search (closed interval on both sides) */\nfunction binarySearch(nums: number[], target: number): number {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let i = 0,\n        j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        // Calculate the midpoint index m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    return -1; // Target element not found, return -1\n}\n
    binary_search.dart
    /* Binary search (closed interval on both sides) */\nint binarySearch(List<int> nums, int target) {\n  // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n  int i = 0, j = nums.length - 1;\n  // Loop, exit when the search interval is empty (empty when i > j)\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      // This means target is in the interval [m+1, j]\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // This means target is in the interval [i, m-1]\n      j = m - 1;\n    } else {\n      // Found the target element, return its index\n      return m;\n    }\n  }\n  // Target element not found, return -1\n  return -1;\n}\n
    binary_search.rs
    /* Binary search (closed interval on both sides) */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let mut i = 0;\n    let mut j = nums.len() as i32 - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.c
    /* Binary search (closed interval on both sides) */\nint binarySearch(int *nums, int len, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = len - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.kt
    /* Binary search (closed interval on both sides) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    var i = 0\n    var j = nums.size - 1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j]\n            i = m + 1\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1\n        else  // Found the target element, return its index\n            return m\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.rb
    ### Binary search (closed interval) ###\ndef binary_search(nums, target)\n  # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n  i, j = 0, nums.length - 1\n\n  # Loop, exit when the search interval is empty (empty when i > j)\n  while i <= j\n    # In theory, Ruby numbers can be infinitely large (limited by memory), no need to consider overflow\n    m = (i + j) / 2   # Calculate the midpoint index m\n\n    if nums[m] < target\n      i = m + 1 # This means target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # This means target is in the interval [i, m-1]\n    else\n      return m  # Found the target element, return its index\n    end\n  end\n\n  -1  # Target element not found, return -1\nend\n

    Time complexity is \\(O(\\log n)\\): In the binary search loop, the interval is reduced by half each round, so the number of iterations is \\(\\log_2 n\\).

    Space complexity is \\(O(1)\\): Pointers \\(i\\) and \\(j\\) use constant-size space.

    ","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search/#1011-interval-representation-methods","level":2,"title":"10.1.1   Interval Representation Methods","text":"

    In addition to the closed interval mentioned above, another common interval representation is the \"left-closed right-open\" interval, defined as \\([0, n)\\), meaning that the left boundary is inclusive while the right boundary is exclusive. Under this representation, the interval \\([i, j)\\) is empty when \\(i = j\\).

    We can implement a binary search algorithm with the same functionality based on this representation:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
    def binary_search_lcro(nums: list[int], target: int) -> int:\n    \"\"\"Binary search (left-closed right-open interval)\"\"\"\n    # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    i, j = 0, len(nums)\n    # Loop, exit when the search interval is empty (empty when i = j)\n    while i < j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # This means target is in the interval [m+1, j)\n        elif nums[m] > target:\n            j = m  # This means target is in the interval [i, m)\n        else:\n            return m  # Found the target element, return its index\n    return -1  # Target element not found, return -1\n
    binary_search.cpp
    /* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.size();\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.java
    /* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(int[] nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.cs
    /* Binary search (left-closed right-open interval) */\nint BinarySearchLCRO(int[] nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.Length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2;   // Calculate the midpoint index m\n        if (nums[m] < target)      // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else                       // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.go
    /* Binary search (left-closed right-open interval) */\nfunc binarySearchLCRO(nums []int, target int) int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    i, j := 0, len(nums)\n    // Loop, exit when the search interval is empty (empty when i = j)\n    for i < j {\n        m := i + (j-i)/2      // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m)\n            j = m\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.swift
    /* Binary search (left-closed right-open interval) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -> Int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    var i = nums.startIndex\n    var j = nums.endIndex\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while i < j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m)\n            j = m\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.js
    /* Binary search (left-closed right-open interval) */\nfunction binarySearchLCRO(nums, target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let i = 0,\n        j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        // Calculate midpoint index m, use parseInt() to round down\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target)\n            // This means target is in the interval [i, m)\n            j = m;\n        // Found the target element, return its index\n        else return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.ts
    /* Binary search (left-closed right-open interval) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let i = 0,\n        j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        // Calculate the midpoint index m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // This means target is in the interval [i, m)\n            j = m;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    return -1; // Target element not found, return -1\n}\n
    binary_search.dart
    /* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(List<int> nums, int target) {\n  // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n  int i = 0, j = nums.length;\n  // Loop, exit when the search interval is empty (empty when i = j)\n  while (i < j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      // This means target is in the interval [m+1, j)\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // This means target is in the interval [i, m)\n      j = m;\n    } else {\n      // Found the target element, return its index\n      return m;\n    }\n  }\n  // Target element not found, return -1\n  return -1;\n}\n
    binary_search.rs
    /* Binary search (left-closed right-open interval) */\nfn binary_search_lcro(nums: &[i32], target: i32) -> i32 {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let mut i = 0;\n    let mut j = nums.len() as i32;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while i < j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // This means target is in the interval [i, m)\n            j = m;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.c
    /* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(int *nums, int len, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = len;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.kt
    /* Binary search (left-closed right-open interval) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    var i = 0\n    var j = nums.size\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j)\n            i = m + 1\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m\n        else  // Found the target element, return its index\n            return m\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.rb
    ### Binary search (left-closed right-open interval) ###\ndef binary_search_lcro(nums, target)\n  # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n  i, j = 0, nums.length\n\n  # Loop, exit when the search interval is empty (empty when i = j)\n  while i < j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # This means target is in the interval [m+1, j)\n    elsif nums[m] > target\n      j = m - 1 # This means target is in the interval [i, m)\n    else\n      return m  # Found the target element, return its index\n    end\n  end\n\n  -1  # Target element not found, return -1\nend\n

    As shown in Figure 10-3, under the two interval representations, the initialization, loop condition, and interval narrowing operations of the binary search algorithm are all different.

    Since both the left and right boundaries in the \"closed interval\" representation are defined as closed, the operations to narrow the interval through pointers \\(i\\) and \\(j\\) are also symmetric. This makes it less error-prone, so the \"closed interval\" approach is generally recommended.

    Figure 10-3   Two interval definitions

    ","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search/#1012-advantages-and-limitations","level":2,"title":"10.1.2   Advantages and Limitations","text":"

    Binary search offers good performance in both time and space.

    • Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size \\(n = 2^{20}\\), linear search requires \\(2^{20} = 1048576\\) iterations, while binary search only needs \\(\\log_2 2^{20} = 20\\) iterations.
    • Binary search requires no extra space. Compared to searching algorithms that require additional space (such as hash-based search), binary search is more space-efficient.

    However, binary search is not suitable for all situations, mainly for the following reasons:

    • Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of \\(O(n \\log n)\\), which is higher than both linear search and binary search. For scenarios with frequent element insertions, keeping the array sorted requires inserting elements at specific positions with a time complexity of \\(O(n)\\), which is also very expensive.
    • Binary search is only applicable to arrays. Binary search requires non-contiguous, jump-style access to elements, and this kind of access is inefficient in linked lists, making it unsuitable for linked lists or linked-list-based data structures.
    • For small data volumes, linear search performs better. In linear search, each round requires only 1 comparison operation; while in binary search, it requires 1 addition, 1 division, 1-3 comparison operations, and 1 addition (subtraction), totaling 4-6 unit operations. Therefore, when the data volume \\(n\\) is small, linear search is actually faster than binary search.
    ","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   Binary Search Boundaries","text":"","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031-finding-the-left-boundary","level":2,"title":"10.3.1   Finding the Left Boundary","text":"

    Question

    Given a sorted array nums of length \\(n\\) that may contain duplicate elements, return the index of the leftmost occurrence of target. If the array does not contain target, return \\(-1\\).

    Recall the method for finding the insertion point with binary search. After the search completes, \\(i\\) points to the leftmost target, so finding the insertion point is essentially finding the index of the leftmost target.

    Consider implementing the left boundary search using the insertion point finding function. Note that the array may not contain target, which could result in the following two cases:

    • The insertion point index \\(i\\) is out of bounds.
    • The element nums[i] is not equal to target.

    When either of these situations occurs, simply return \\(-1\\). The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
    def binary_search_left_edge(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for the leftmost target\"\"\"\n    # Equivalent to finding the insertion point of target\n    i = binary_search_insertion(nums, target)\n    # Target not found, return -1\n    if i == len(nums) or nums[i] != target:\n        return -1\n    # Found target, return index i\n    return i\n
    binary_search_edge.cpp
    /* Binary search for the leftmost target */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.size() || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.java
    /* Binary search for the leftmost target */\nint binarySearchLeftEdge(int[] nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binary_search_insertion.binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.length || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.cs
    /* Binary search for the leftmost target */\nint BinarySearchLeftEdge(int[] nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.Length || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.go
    /* Binary search for the leftmost target */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n    // Equivalent to finding the insertion point of target\n    i := binarySearchInsertion(nums, target)\n    // Target not found, return -1\n    if i == len(nums) || nums[i] != target {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
    binary_search_edge.swift
    /* Binary search for the leftmost target */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -> Int {\n    // Equivalent to finding the insertion point of target\n    let i = binarySearchInsertion(nums: nums, target: target)\n    // Target not found, return -1\n    if i == nums.endIndex || nums[i] != target {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
    binary_search_edge.js
    /* Binary search for the leftmost target */\nfunction binarySearchLeftEdge(nums, target) {\n    // Equivalent to finding the insertion point of target\n    const i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.ts
    /* Binary search for the leftmost target */\nfunction binarySearchLeftEdge(nums: Array<number>, target: number): number {\n    // Equivalent to finding the insertion point of target\n    const i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.dart
    /* Binary search for the leftmost target */\nint binarySearchLeftEdge(List<int> nums, int target) {\n  // Equivalent to finding the insertion point of target\n  int i = binarySearchInsertion(nums, target);\n  // Target not found, return -1\n  if (i == nums.length || nums[i] != target) {\n    return -1;\n  }\n  // Found target, return index i\n  return i;\n}\n
    binary_search_edge.rs
    /* Binary search for the leftmost target */\nfn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {\n    // Equivalent to finding the insertion point of target\n    let i = binary_search_insertion(nums, target);\n    // Target not found, return -1\n    if i == nums.len() as i32 || nums[i as usize] != target {\n        return -1;\n    }\n    // Found target, return index i\n    i\n}\n
    binary_search_edge.c
    /* Binary search for the leftmost target */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binarySearchInsertion(nums, numSize, target);\n    // Target not found, return -1\n    if (i == numSize || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.kt
    /* Binary search for the leftmost target */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n    // Equivalent to finding the insertion point of target\n    val i = binarySearchInsertion(nums, target)\n    // Target not found, return -1\n    if (i == nums.size || nums[i] != target) {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
    binary_search_edge.rb
    ### Binary search leftmost target ###\ndef binary_search_left_edge(nums, target)\n  # Equivalent to finding the insertion point of target\n  i = binary_search_insertion(nums, target)\n\n  # Target not found, return -1\n  return -1 if i == nums.length || nums[i] != target\n\n  i # Found target, return index i\nend\n
    ","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032-finding-the-right-boundary","level":2,"title":"10.3.2   Finding the Right Boundary","text":"

    So how do we find the rightmost target? The most direct approach is to modify the code and replace the pointer shrinking operation in the nums[m] == target case. The code is omitted here; interested readers can implement it themselves.

    Below we introduce two more clever methods.

    ","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1-reusing-left-boundary-search","level":3,"title":"1.   Reusing Left Boundary Search","text":"

    In fact, we can use the function for finding the leftmost target to find the rightmost target. The specific method is: convert finding the rightmost target into finding the leftmost target + 1.

    As shown in Figure 10-7, after the search completes, the pointer \\(i\\) points to the leftmost target + 1 (if it exists), while \\(j\\) points to the rightmost target, so we can return \\(j\\).

    Figure 10-7   Converting right boundary search to left boundary search

    Note that the returned insertion point is \\(i\\), so we need to subtract \\(1\\) from it to obtain \\(j\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
    def binary_search_right_edge(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for the rightmost target\"\"\"\n    # Convert to finding the leftmost target + 1\n    i = binary_search_insertion(nums, target + 1)\n    # j points to the rightmost target, i points to the first element greater than target\n    j = i - 1\n    # Target not found, return -1\n    if j == -1 or nums[j] != target:\n        return -1\n    # Found target, return index j\n    return j\n
    binary_search_edge.cpp
    /* Binary search for the rightmost target */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.java
    /* Binary search for the rightmost target */\nint binarySearchRightEdge(int[] nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.cs
    /* Binary search for the rightmost target */\nint BinarySearchRightEdge(int[] nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.go
    /* Binary search for the rightmost target */\nfunc binarySearchRightEdge(nums []int, target int) int {\n    // Convert to finding the leftmost target + 1\n    i := binarySearchInsertion(nums, target+1)\n    // j points to the rightmost target, i points to the first element greater than target\n    j := i - 1\n    // Target not found, return -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
    binary_search_edge.swift
    /* Binary search for the rightmost target */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -> Int {\n    // Convert to finding the leftmost target + 1\n    let i = binarySearchInsertion(nums: nums, target: target + 1)\n    // j points to the rightmost target, i points to the first element greater than target\n    let j = i - 1\n    // Target not found, return -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
    binary_search_edge.js
    /* Binary search for the rightmost target */\nfunction binarySearchRightEdge(nums, target) {\n    // Convert to finding the leftmost target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    const j = i - 1;\n    // Target not found, return -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.ts
    /* Binary search for the rightmost target */\nfunction binarySearchRightEdge(nums: Array<number>, target: number): number {\n    // Convert to finding the leftmost target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    const j = i - 1;\n    // Target not found, return -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.dart
    /* Binary search for the rightmost target */\nint binarySearchRightEdge(List<int> nums, int target) {\n  // Convert to finding the leftmost target + 1\n  int i = binarySearchInsertion(nums, target + 1);\n  // j points to the rightmost target, i points to the first element greater than target\n  int j = i - 1;\n  // Target not found, return -1\n  if (j == -1 || nums[j] != target) {\n    return -1;\n  }\n  // Found target, return index j\n  return j;\n}\n
    binary_search_edge.rs
    /* Binary search for the rightmost target */\nfn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {\n    // Convert to finding the leftmost target + 1\n    let i = binary_search_insertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    let j = i - 1;\n    // Target not found, return -1\n    if j == -1 || nums[j as usize] != target {\n        return -1;\n    }\n    // Found target, return index j\n    j\n}\n
    binary_search_edge.c
    /* Binary search for the rightmost target */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binarySearchInsertion(nums, numSize, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.kt
    /* Binary search for the rightmost target */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n    // Convert to finding the leftmost target + 1\n    val i = binarySearchInsertion(nums, target + 1)\n    // j points to the rightmost target, i points to the first element greater than target\n    val j = i - 1\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
    binary_search_edge.rb
    ### Binary search rightmost target ###\ndef binary_search_right_edge(nums, target)\n  # Convert to finding the leftmost target + 1\n  i = binary_search_insertion(nums, target + 1)\n\n  # j points to the rightmost target, i points to the first element greater than target\n  j = i - 1\n\n  # Target not found, return -1\n  return -1 if j == -1 || nums[j] != target\n\n  j # Found target, return index j\nend\n
    ","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2-converting-to-element-search","level":3,"title":"2.   Converting to Element Search","text":"

    We know that when the array does not contain target, \\(i\\) and \\(j\\) will eventually point to the first elements greater than and less than target, respectively.

    Therefore, as shown in Figure 10-8, we can construct an element that does not exist in the array to find the left and right boundaries.

    • Finding the leftmost target: This can be converted to finding target - 0.5 and returning the pointer \\(i\\).
    • Finding the rightmost target: This can be converted to finding target + 0.5 and returning the pointer \\(j\\).

    Figure 10-8   Converting boundary search to element search

    The code is omitted here, but the following two points are worth noting:

    • Since the given array does not contain decimal values, we do not need to worry about how to handle equality.
    • Because this method introduces decimals, the variable target in the function needs to be changed to a floating-point type (Python does not require this change).
    ","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   Binary Search Insertion Point","text":"

    Binary search can be used not only to search for target elements, but also to solve many variant problems, such as finding the insertion position of a target element.

    ","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021-case-without-duplicate-elements","level":2,"title":"10.2.1   Case Without Duplicate Elements","text":"

    Question

    Given a sorted array nums of length \\(n\\) and an element target, where the array contains no duplicate elements, insert target into nums while maintaining its sorted order. If target already exists in the array, insert it to its left. Return the index of target after insertion. An example is shown below.

    Figure 10-4   Binary search insertion point example data

    If we want to reuse the binary search code from the previous section, we need to answer the following two questions.

    Question 1: When the array contains target, is the insertion point index the same as that element's index?

    The problem requires inserting target to the left of equal elements, which means the newly inserted target replaces the position of the original target. In other words, when the array contains target, the insertion point index is the index of that target.

    Question 2: When the array does not contain target, what is the insertion point index?

    To analyze this further, consider the binary search process: when nums[m] < target, \\(i\\) moves, meaning that pointer \\(i\\) is approaching elements greater than or equal to target. Similarly, pointer \\(j\\) is always approaching elements less than or equal to target.

    Therefore, when the binary search ends, \\(i\\) must point to the first element greater than target, and \\(j\\) must point to the first element less than target. It follows that when the array does not contain target, the insertion index is \\(i\\). The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
    def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for insertion point (no duplicate elements)\"\"\"\n    i, j = 0, len(nums) - 1  # Initialize closed interval [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target is in the interval [i, m-1]\n        else:\n            return m  # Found target, return insertion point m\n    # Target not found, return insertion point i\n    return i\n
    binary_search_insertion.cpp
    /* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.java
    /* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.cs
    /* Binary search for insertion point (no duplicate elements) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.go
    /* Binary search for insertion point (no duplicate elements) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n    // Initialize closed interval [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Calculate the midpoint index m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target is in the interval [i, m-1]\n            j = m - 1\n        } else {\n            // Found target, return insertion point m\n            return m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
    binary_search_insertion.swift
    /* Binary search for insertion point (no duplicate elements) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [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 // Calculate the midpoint index m\n        if nums[m] < target {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            return m // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
    binary_search_insertion.js
    /* Binary search for insertion point (no duplicate elements) */\nfunction binarySearchInsertionSimple(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.ts
    /* Binary search for insertion point (no duplicate elements) */\nfunction binarySearchInsertionSimple(\n    nums: Array<number>,\n    target: number\n): number {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.dart
    /* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      i = m + 1; // target is in the interval [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target is in the interval [i, m-1]\n    } else {\n      return m; // Found target, return insertion point m\n    }\n  }\n  // Target not found, return insertion point i\n  return i;\n}\n
    binary_search_insertion.rs
    /* Binary search for insertion point (no duplicate elements) */\nfn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m;\n        }\n    }\n    // Target not found, return insertion point i\n    i\n}\n
    binary_search_insertion.c
    /* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.kt
    /* Binary search for insertion point (no duplicate elements) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            return m // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
    binary_search_insertion.rb
    ### Binary search insertion point (no duplicates) ###\ndef binary_search_insertion_simple(nums, target)\n  # Initialize closed interval [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target is in the interval [i, m-1]\n    else\n      return m  # Found target, return insertion point m\n    end\n  end\n\n  i # Target not found, return insertion point i\nend\n
    ","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022-case-with-duplicate-elements","level":2,"title":"10.2.2   Case with Duplicate Elements","text":"

    Question

    Based on the previous problem, assume the array may contain duplicate elements, with everything else remaining the same.

    Suppose there are multiple target elements in the array. Ordinary binary search can only return the index of one target, and cannot determine how many target elements are to the left and right of that element.

    The problem requires inserting the target element at the leftmost position, so we need to find the index of the leftmost target in the array. A straightforward initial approach is to follow the steps shown in Figure 10-5:

    1. Perform binary search to obtain the index of any target, denoted as \\(k\\).
    2. Starting from index \\(k\\), perform linear traversal to the left, and return when the leftmost target is found.

    Figure 10-5   Linear search for insertion point of duplicate elements

    Although this method works, it includes linear search, resulting in a time complexity of \\(O(n)\\). When the array contains many duplicate target elements, this method is very inefficient.

    Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains unchanged: in each iteration, we first compute the midpoint index \\(m\\), then compare target with nums[m], leading to the following cases:

    • When nums[m] < target or nums[m] > target, it means target has not been found yet, so use the standard interval-shrinking operation of binary search to move pointers \\(i\\) and \\(j\\) closer to target.
    • When nums[m] == target, it means elements less than target are in the interval \\([i, m - 1]\\), so use \\(j = m - 1\\) to shrink the interval, thereby moving pointer \\(j\\) closer to elements less than target.

    After the loop completes, \\(i\\) points to the leftmost target, and \\(j\\) points to the first element less than target, so index \\(i\\) is the insertion point.

    <1><2><3><4><5><6><7><8>

    Figure 10-6   Steps for binary search insertion point of duplicate elements

    Observe the following code: the branches nums[m] > target and nums[m] == target perform the same operation, so they can be merged.

    Even so, we can still keep the conditional branches expanded, as the logic is clearer and more readable.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
    def binary_search_insertion(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for insertion point (with duplicate elements)\"\"\"\n    i, j = 0, len(nums) - 1  # Initialize closed interval [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target is in the interval [i, m-1]\n        else:\n            j = m - 1  # The first element less than target is in the interval [i, m-1]\n    # Return insertion point i\n    return i\n
    binary_search_insertion.cpp
    /* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.java
    /* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.cs
    /* Binary search for insertion point (with duplicate elements) */\nint BinarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.go
    /* Binary search for insertion point (with duplicate elements) */\nfunc binarySearchInsertion(nums []int, target int) int {\n    // Initialize closed interval [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Calculate the midpoint index m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target is in the interval [i, m-1]\n            j = m - 1\n        } else {\n            // The first element less than target is in the interval [i, m-1]\n            j = m - 1\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
    binary_search_insertion.swift
    /* Binary search for insertion point (with duplicate elements) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [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 // Calculate the midpoint index m\n        if nums[m] < target {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            j = m - 1 // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
    binary_search_insertion.js
    /* Binary search for insertion point (with duplicate elements) */\nfunction binarySearchInsertion(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.ts
    /* Binary search for insertion point (with duplicate elements) */\nfunction binarySearchInsertion(nums: Array<number>, target: number): number {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.dart
    /* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      i = m + 1; // target is in the interval [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target is in the interval [i, m-1]\n    } else {\n      j = m - 1; // The first element less than target is in the interval [i, m-1]\n    }\n  }\n  // Return insertion point i\n  return i;\n}\n
    binary_search_insertion.rs
    /* Binary search for insertion point (with duplicate elements) */\npub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    i\n}\n
    binary_search_insertion.c
    /* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.kt
    /* Binary search for insertion point (with duplicate elements) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            j = m - 1 // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
    binary_search_insertion.rb
    ### Binary search insertion point (with duplicates) ###\ndef binary_search_insertion(nums, target)\n  # Initialize closed interval [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target is in the interval [i, m-1]\n    else\n      j = m - 1 # The first element less than target is in the interval [i, m-1]\n    end\n  end\n\n  i # Return insertion point i\nend\n

    Tip

    The code in this section uses the \"closed interval\" approach throughout. Interested readers can implement the \"left-closed, right-open\" approach themselves.

    Overall, binary search is simply a matter of setting separate search targets for pointers \\(i\\) and \\(j\\). The target may be a specific element (such as target) or a range of elements (such as elements less than target).

    With each iteration of binary search, pointers \\(i\\) and \\(j\\) gradually approach their preset targets. Ultimately, they either find the answer or stop after crossing the boundary.

    ","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   Hash Optimization Strategy","text":"

    In algorithm problems, we often reduce the time complexity of algorithms by replacing linear search with hash-based search. Let's use an algorithm problem to deepen our understanding.

    Question

    Given an integer array nums and a target value target, find two elements in the array whose sum is target, and return their indices. Any solution will do.

    ","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041-linear-search-trading-time-for-space","level":2,"title":"10.4.1   Linear Search: Trading Time for Space","text":"

    Consider directly traversing all possible combinations. As shown in Figure 10-9, we use nested loops and check in each iteration whether the sum of two integers is target. If so, return their indices.

    Figure 10-9   Linear search solution for two sum

    The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
    def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"Method 1: Brute force enumeration\"\"\"\n    # Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nint[] twoSumBruteForce(int[] nums, int target) {\n    int size = nums.length;\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nint[] TwoSumBruteForce(int[] nums, int target) {\n    int size = nums.Length;\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nfunc twoSumBruteForce(nums []int, target int) []int {\n    size := len(nums)\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nfunc twoSumBruteForce(nums: [Int], target: Int) -> [Int] {\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nfunction twoSumBruteForce(nums, target) {\n    const n = nums.length;\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n    const n = nums.length;\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nList<int> twoSumBruteForce(List<int> nums, int target) {\n  int size = nums.length;\n  // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\npub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    let size = nums.len();\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\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
    /* Method 1: Brute force enumeration */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Two nested loops, time complexity is 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
    ### Method 1: Brute force enumeration ###\ndef two_sum_brute_force(nums, target)\n  # Two nested loops, time complexity is 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

    This method has a time complexity of \\(O(n^2)\\) and a space complexity of \\(O(1)\\), making it very time-consuming on large inputs.

    ","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042-hash-based-search-trading-space-for-time","level":2,"title":"10.4.2   Hash-Based Search: Trading Space for Time","text":"

    Consider using a hash table whose keys are array elements and whose values are their indices. Traverse the array and perform the steps shown in Figure 10-10 in each iteration:

    1. Check if the number target - nums[i] is in the hash table. If so, directly return the indices of these two elements.
    2. Add the key-value pair nums[i] and index i to the hash table.
    <1><2><3>

    Figure 10-10   Hash table solution for two sum

    The implementation is shown below and requires only a single loop:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
    def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n    \"\"\"Method 2: Auxiliary hash table\"\"\"\n    # Auxiliary hash table, space complexity is O(n)\n    dic = {}\n    # Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Auxiliary hash table, space complexity is O(n)\n    unordered_map<int, int> dic;\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nint[] twoSumHashTable(int[] nums, int target) {\n    int size = nums.length;\n    // Auxiliary hash table, space complexity is O(n)\n    Map<Integer, Integer> dic = new HashMap<>();\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nint[] TwoSumHashTable(int[] nums, int target) {\n    int size = nums.Length;\n    // Auxiliary hash table, space complexity is O(n)\n    Dictionary<int, int> dic = [];\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nfunc twoSumHashTable(nums []int, target int) []int {\n    // Auxiliary hash table, space complexity is O(n)\n    hashTable := map[int]int{}\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nfunc twoSumHashTable(nums: [Int], target: Int) -> [Int] {\n    // Auxiliary hash table, space complexity is O(n)\n    var dic: [Int: Int] = [:]\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nfunction twoSumHashTable(nums, target) {\n    // Auxiliary hash table, space complexity is O(n)\n    let m = {};\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n    // Auxiliary hash table, space complexity is O(n)\n    let m: Map<number, number> = new Map();\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nList<int> twoSumHashTable(List<int> nums, int target) {\n  int size = nums.length;\n  // Auxiliary hash table, space complexity is O(n)\n  Map<int, int> dic = HashMap();\n  // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\npub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    // Auxiliary hash table, space complexity is O(n)\n    let mut dic = HashMap::new();\n    // Single loop, time complexity is 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
    /* Hash table */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Implemented using uthash.h\n} HashTable;\n\n/* Hash table lookup */\nHashTable *find(HashTable *h, int key) {\n    HashTable *tmp;\n    HASH_FIND_INT(h, &key, tmp);\n    return tmp;\n}\n\n/* Hash table element insertion */\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/* Method 2: Auxiliary hash table */\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
    /* Method 2: Auxiliary hash table */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Auxiliary hash table, space complexity is O(n)\n    val dic = HashMap<Int, Int>()\n    // Single loop, time complexity is 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
    ### Method 2: Auxiliary hash table ###\ndef two_sum_hash_table(nums, target)\n  # Auxiliary hash table, space complexity is O(n)\n  dic = {}\n  # Single loop, time complexity is 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

    This method reduces the time complexity from \\(O(n^2)\\) to \\(O(n)\\) through hash-based search, greatly improving runtime efficiency.

    Since an additional hash table needs to be maintained, the space complexity is \\(O(n)\\). Nevertheless, this method offers a more balanced overall time-space trade-off, making it the optimal solution to this problem.

    ","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   Searching Algorithms Revisited","text":"

    Searching algorithms are used to search for one or a group of elements that meet specific conditions in data structures (such as arrays, linked lists, trees, or graphs).

    Searching algorithms can be divided into the following two categories based on their implementation approach:

    • Locating target elements by traversing the data structure, such as traversing arrays, linked lists, trees, and graphs.
    • Achieving efficient element lookup by leveraging the way data is organized or prior information about the data, such as binary search, hash-based search, and binary search tree search.

    As these topics have already been introduced in earlier chapters, searching algorithms should already be familiar to us. In this section, we revisit them from a more systematic perspective.

    ","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051-brute-force-search","level":2,"title":"10.5.1   Brute-Force Search","text":"

    Brute-force search locates target elements by traversing each element of the data structure.

    • \"Linear search\" is applicable to linear data structures such as arrays and linked lists. It starts from one end of the data structure and accesses elements one by one until the target element is found or the other end is reached without finding the target element.
    • \"Breadth-first search\" and \"depth-first search\" are two traversal strategies for graphs and trees. Breadth-first search starts from the initial node and searches layer by layer, visiting nodes from near to far. Depth-first search starts from the initial node, follows a path to the end, then backtracks and tries other paths until the entire data structure is traversed.

    The advantage of brute-force search is that it is simple and has good generality, requiring no data preprocessing or additional data structures.

    However, the time complexity of such algorithms is \\(O(n)\\), where \\(n\\) is the number of elements, so performance is poor when dealing with large amounts of data.

    ","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052-adaptive-search","level":2,"title":"10.5.2   Adaptive Search","text":"

    Adaptive search leverages properties of the data itself (such as sorted order) to optimize the search process and locate target elements more efficiently.

    • \"Binary search\" uses the orderliness of data to achieve efficient searching, applicable only to arrays.
    • \"Hash-based search\" uses hash tables to store searchable data as key-value pairs, thereby enabling efficient queries.
    • \"Tree search\" operates on specific tree structures (such as binary search trees), quickly ruling out nodes by comparing node values to locate the target element.

    The advantage of such algorithms is high efficiency, with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\).

    However, using these algorithms often requires data preprocessing. For example, binary search requires pre-sorting the array, while hash-based search and tree search both require additional data structures, and maintaining these data structures also requires extra time and space overhead.

    Tip

    Adaptive search algorithms are often called lookup algorithms, mainly used to quickly retrieve target elements in specific data structures.

    ","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053-search-method-selection","level":2,"title":"10.5.3   Search Method Selection","text":"

    Given a dataset of size \\(n\\), we can use linear search, binary search, tree search, hash-based search, and other methods to search for the target element. The working principles of each method are shown in Figure 10-11.

    Figure 10-11   Multiple search strategies

    The efficiency and characteristics of these methods are summarized in Table 10-1.

    Table 10-1   Comparison of search algorithm efficiency

    Linear search Binary search Tree search Hash-based search Search element \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) Insert element \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Delete element \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Extra space \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) Data preprocessing / Sorting \\(O(n \\log n)\\) Tree building \\(O(n \\log n)\\) Hash table building \\(O(n)\\) Data ordered Unordered Ordered Ordered Unordered

    The choice of search algorithm also depends on data volume, search performance requirements, data query and update frequency, etc.

    Linear search

    • Good generality, requiring no data preprocessing operations. If we need to query the data only once, the preprocessing required by the other three methods can take longer than the linear search itself.
    • Suitable for small data volumes, where time complexity has less impact on efficiency.
    • Suitable for scenarios with high data update frequency, as this method does not require any additional data maintenance.

    Binary search

    • Suitable for large datasets, with stable performance and a worst-case time complexity of \\(O(\\log n)\\).
    • Data volume cannot be too large, as storing arrays requires contiguous memory space.
    • Not suitable for scenarios with frequent data insertion and deletion, as maintaining a sorted array has high overhead.

    Hash-based search

    • Suitable for scenarios with high query performance requirements, with an average time complexity of \\(O(1)\\).
    • Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain the data in sorted order.
    • High dependence on hash functions and hash collision handling strategies, with significant risk of performance degradation.
    • Not suitable for excessively large data volumes, as hash tables require extra space to minimize collisions and thus provide good query performance.

    Tree search

    • Suitable for massive datasets, as tree nodes are stored non-contiguously in memory.
    • Suitable for scenarios that require maintaining ordered data or performing range searches.
    • During continuous node insertion and deletion, binary search trees may become skewed, degrading time complexity to \\(O(n)\\).
    • If AVL trees or red-black trees are used, all operations can consistently run in \\(O(\\log n)\\) time, though maintaining tree balance adds extra overhead.
    ","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   Summary","text":"","path":["Chapter 10. Searching","10.6   Summary"],"tags":[]},{"location":"chapter_searching/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Binary search relies on ordered data and searches by repeatedly halving the search interval. It requires the input data to be sorted and applies only to arrays or array-based data structures.
    • Brute-force search locates data by traversing the data structure. Linear search applies to arrays and linked lists, while breadth-first search and depth-first search apply to graphs and trees. These algorithms are broadly applicable and require no data preprocessing, but their relatively high time complexity is \\(O(n)\\).
    • Hash-based search, tree search, and binary search are efficient search methods that can quickly locate target elements in specific data structures. Such algorithms are highly efficient with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\), but typically require additional data structures.
    • In practice, we need to analyze factors such as data scale, search performance requirements, and data query and update frequency to choose the appropriate search method.
    • Linear search is suitable for small datasets or data that is updated frequently; binary search is suitable for large sorted datasets; hash-based search is suitable when high query efficiency is required and range queries are unnecessary; tree search is suitable for large dynamic datasets that must maintain order and support range queries.
    • Replacing linear search with hash-based search is a commonly used strategy to optimize runtime, reducing time complexity from \\(O(n)\\) to \\(O(1)\\).
    ","path":["Chapter 10. Searching","10.6   Summary"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"Chapter 11.   Sorting","text":"

    Abstract

    Sorting is like a magic key that transforms chaos into order, enabling us to understand and process data more efficiently.

    From simple ascending order to more complex classification schemes, sorting reveals the harmonious beauty of data.

    ","path":["Chapter 11. Sorting","Chapter 11.   Sorting"],"tags":[]},{"location":"chapter_sorting/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 11.1   Sorting Algorithm
    • 11.2   Selection Sort
    • 11.3   Bubble Sort
    • 11.4   Insertion Sort
    • 11.5   Quick Sort
    • 11.6   Merge Sort
    • 11.7   Heap Sort
    • 11.8   Bucket Sort
    • 11.9   Counting Sort
    • 11.10   Radix Sort
    • 11.11   Summary
    ","path":["Chapter 11. Sorting","Chapter 11.   Sorting"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   Bubble Sort","text":"

    Bubble sort sorts an array by continuously comparing and swapping adjacent elements. This process resembles bubbles rising from the bottom to the top, hence the name bubble sort.

    As shown in Figure 11-4, the bubbling process can be simulated using element swaps: starting from the leftmost end of the array and traversing to the right, compare each pair of adjacent elements, and if \"left element > right element\", swap them. After the traversal is complete, the largest element is moved to the rightmost end of the array.

    <1><2><3><4><5><6><7>

    Figure 11-4   Simulating bubble sort using element swaps

    ","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131-algorithm-flow","level":2,"title":"11.3.1   Algorithm Flow","text":"

    Assume the array has length \\(n\\). The steps of bubble sort are shown in Figure 11-5.

    1. First, perform \"bubbling\" on \\(n\\) elements, swapping the largest element of the array to its correct position.
    2. Next, perform \"bubbling\" on the remaining \\(n - 1\\) elements, swapping the second largest element to its correct position.
    3. And so on. After \\(n - 1\\) rounds of \"bubbling\", the largest \\(n - 1\\) elements have all been swapped to their correct positions.
    4. The only remaining element must be the smallest element, requiring no sorting, so the array sorting is complete.

    Figure 11-5   Bubble sort flow

    Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
    def bubble_sort(nums: list[int]):\n    \"\"\"Bubble sort\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [0, i]\n    for i in range(n - 1, 0, -1):\n        # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n
    bubble_sort.cpp
    /* Bubble sort */\nvoid bubbleSort(vector<int> &nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                // Using std::swap() function here\n                swap(nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
    bubble_sort.java
    /* Bubble sort */\nvoid bubbleSort(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and 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
    /* Bubble sort */\nvoid BubbleSort(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
    bubble_sort.go
    /* Bubble sort */\nfunc bubbleSort(nums []int) {\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n            }\n        }\n    }\n}\n
    bubble_sort.swift
    /* Bubble sort */\nfunc bubbleSort(nums: inout [Int]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swapAt(j, j + 1)\n            }\n        }\n    }\n}\n
    bubble_sort.js
    /* Bubble sort */\nfunction bubbleSort(nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and 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
    /* Bubble sort */\nfunction bubbleSort(nums: number[]): void {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and 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
    /* Bubble sort */\nvoid bubbleSort(List<int> nums) {\n  // Outer loop: unsorted range is [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and 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
    /* Bubble sort */\nfn bubble_sort(nums: &mut [i32]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swap(j, j + 1);\n            }\n        }\n    }\n}\n
    bubble_sort.c
    /* Bubble sort */\nvoid bubbleSort(int nums[], int size) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\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
    /* Bubble sort */\nfun bubbleSort(nums: IntArray) {\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and 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
    ### Bubble sort ###\ndef bubble_sort(nums)\n  n = nums.length\n  # Outer loop: unsorted range is [0, i]\n  for i in (n - 1).downto(1)\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132-efficiency-optimization","level":2,"title":"11.3.2   Efficiency Optimization","text":"

    We can observe that if no swaps occur during a round of \"bubbling\", the array is already sorted and the algorithm can return immediately. Therefore, we can add a flag flag to detect this situation and terminate as soon as it occurs.

    After this optimization, the worst-case and average-case time complexities of bubble sort remain \\(O(n^2)\\); however, when the input array is already sorted, the best-case time complexity becomes \\(O(n)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
    def bubble_sort_with_flag(nums: list[int]):\n    \"\"\"Bubble sort (flag optimization)\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [0, i]\n    for i in range(n - 1, 0, -1):\n        flag = False  # Initialize flag\n        # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n                flag = True  # Record element swap\n        if not flag:\n            break  # No elements were swapped in this round of \"bubbling\", exit directly\n
    bubble_sort.cpp
    /* Bubble sort (flag optimization)*/\nvoid bubbleSortWithFlag(vector<int> &nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        bool flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                // Using std::swap() function here\n                swap(nums[j], nums[j + 1]);\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag)\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.java
    /* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        boolean flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag)\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.cs
    /* Bubble sort (flag optimization) */\nvoid BubbleSortWithFlag(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        bool flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                flag = true;  // Record element swap\n            }\n        }\n        if (!flag) break;     // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.go
    /* Bubble sort (flag optimization) */\nfunc bubbleSortWithFlag(nums []int) {\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        flag := false // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n                flag = true // Record element swap\n            }\n        }\n        if flag == false { // No elements were swapped in this round of \"bubbling\", exit directly\n            break\n        }\n    }\n}\n
    bubble_sort.swift
    /* Bubble sort (flag optimization) */\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        var flag = false // Initialize flag\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swapAt(j, j + 1)\n                flag = true // Record element swap\n            }\n        }\n        if !flag { // No elements were swapped in this round of \"bubbling\", exit directly\n            break\n        }\n    }\n}\n
    bubble_sort.js
    /* Bubble sort (flag optimization) */\nfunction bubbleSortWithFlag(nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.ts
    /* Bubble sort (flag optimization) */\nfunction bubbleSortWithFlag(nums: number[]): void {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.dart
    /* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(List<int> nums) {\n  // Outer loop: unsorted range is [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    bool flag = false; // Initialize flag\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        flag = true; // Record element swap\n      }\n    }\n    if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n  }\n}\n
    bubble_sort.rs
    /* Bubble sort (flag optimization) */\nfn bubble_sort_with_flag(nums: &mut [i32]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        let mut flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swap(j, j + 1);\n                flag = true; // Record element swap\n            }\n        }\n        if !flag {\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n        };\n    }\n}\n
    bubble_sort.c
    /* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(int nums[], int size) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        bool flag = false;\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\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
    /* Bubble sort (flag optimization) */\nfun bubbleSortWithFlag(nums: IntArray) {\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        var flag = false // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                flag = true // Record element swap\n            }\n        }\n        if (!flag) break // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.rb
    ### Bubble sort (flag optimization) ###\ndef bubble_sort_with_flag(nums)\n  n = nums.length\n  # Outer loop: unsorted range is [0, i]\n  for i in (n - 1).downto(1)\n    flag = false # Initialize flag\n\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n        flag = true # Record element swap\n      end\n    end\n\n    break unless flag # No elements were swapped in this round of \"bubbling\", exit directly\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133-algorithm-characteristics","level":2,"title":"11.3.3   Algorithm Characteristics","text":"
    • Time complexity is \\(O(n^2)\\); adaptive: In successive rounds of \"bubbling\", the traversed portion of the array has lengths \\(n - 1\\), \\(n - 2\\), \\(\\dots\\), \\(2\\), \\(1\\), for a total of \\((n - 1) n / 2\\). After introducing the flag optimization, the best-case time complexity can reach \\(O(n)\\).
    • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
    • Stable sorting: Equal elements are not swapped during \"bubbling\".
    ","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   Bucket Sort","text":"

    The sorting algorithms discussed earlier are all comparison-based sorting algorithms, which sort by comparing the relative order of elements. The time complexity of such algorithms cannot beat \\(O(n \\log n)\\). Next, we will explore several non-comparison sorting algorithms, whose time complexity can be linear.

    Bucket sort is a typical application of the divide-and-conquer strategy. It works by creating a sequence of ordered buckets, each corresponding to a data range, and distributing the data evenly among them. The elements within each bucket are then sorted separately. Finally, all buckets are merged in order.

    ","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181-algorithm-flow","level":2,"title":"11.8.1   Algorithm Flow","text":"

    Consider an array of length \\(n\\), whose elements are floating-point numbers in the range \\([0, 1)\\). The flow of bucket sort is shown in Figure 11-13.

    1. Initialize \\(k\\) buckets and distribute the \\(n\\) elements into the \\(k\\) buckets.
    2. Sort each bucket separately (here we use the built-in sorting function of the programming language).
    3. Merge the results in order from smallest to largest bucket.

    Figure 11-13   Bucket sort algorithm flow

    The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py
    def bucket_sort(nums: list[float]):\n    \"\"\"Bucket sort\"\"\"\n    # Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    k = len(nums) // 2\n    buckets = [[] for _ in range(k)]\n    # 1. Distribute array elements into various buckets\n    for num in nums:\n        # Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        i = int(num * k)\n        # Add num to bucket i\n        buckets[i].append(num)\n    # 2. Sort each bucket\n    for bucket in buckets:\n        # Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort()\n    # 3. Traverse buckets to merge results\n    i = 0\n    for bucket in buckets:\n        for num in bucket:\n            nums[i] = num\n            i += 1\n
    bucket_sort.cpp
    /* Bucket sort */\nvoid bucketSort(vector<float> &nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    int k = nums.size() / 2;\n    vector<vector<float>> buckets(k);\n    // 1. Distribute array elements into various buckets\n    for (float num : nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = num * k;\n        // Add num to bucket bucket_idx\n        buckets[i].push_back(num);\n    }\n    // 2. Sort each bucket\n    for (vector<float> &bucket : buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        sort(bucket.begin(), bucket.end());\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nvoid bucketSort(float[] nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\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. Distribute array elements into various buckets\n    for (float num : nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = (int) (num * k);\n        // Add num to bucket i\n        buckets.get(i).add(num);\n    }\n    // 2. Sort each bucket\n    for (List<Float> bucket : buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        Collections.sort(bucket);\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nvoid BucketSort(float[] nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\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. Distribute array elements into various buckets\n    foreach (float num in nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = (int)(num * k);\n        // Add num to bucket i\n        buckets[i].Add(num);\n    }\n    // 2. Sort each bucket\n    foreach (List<float> bucket in buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.Sort();\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfunc bucketSort(nums []float64) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\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. Distribute array elements into various buckets\n    for _, num := range nums {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        i := int(num * float64(k))\n        // Add num to bucket i\n        buckets[i] = append(buckets[i], num)\n    }\n    // 2. Sort each bucket\n    for i := 0; i < k; i++ {\n        // Use built-in slice sorting function, can also be replaced with other sorting algorithms\n        sort.Float64s(buckets[i])\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfunc bucketSort(nums: inout [Double]) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    let k = nums.count / 2\n    var buckets = (0 ..< k).map { _ in [Double]() }\n    // 1. Distribute array elements into various buckets\n    for num in nums {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        let i = Int(num * Double(k))\n        // Add num to bucket i\n        buckets[i].append(num)\n    }\n    // 2. Sort each bucket\n    for i in buckets.indices {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        buckets[i].sort()\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfunction bucketSort(nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    const k = nums.length / 2;\n    const buckets = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Distribute array elements into various buckets\n    for (const num of nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        const i = Math.floor(num * k);\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for (const bucket of buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfunction bucketSort(nums: number[]): void {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    const k = nums.length / 2;\n    const buckets: number[][] = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Distribute array elements into various buckets\n    for (const num of nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        const i = Math.floor(num * k);\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for (const bucket of buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nvoid bucketSort(List<double> nums) {\n  // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n  int k = nums.length ~/ 2;\n  List<List<double>> buckets = List.generate(k, (index) => []);\n\n  // 1. Distribute array elements into various buckets\n  for (double _num in nums) {\n    // Input data range is [0, 1), use _num * k to map to index range [0, k-1]\n    int i = (_num * k).toInt();\n    // Add _num to bucket bucket_idx\n    buckets[i].add(_num);\n  }\n  // 2. Sort each bucket\n  for (List<double> bucket in buckets) {\n    bucket.sort();\n  }\n  // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfn bucket_sort(nums: &mut [f64]) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    let k = nums.len() / 2;\n    let mut buckets = vec![vec![]; k];\n    // 1. Distribute array elements into various buckets\n    for &num in nums.iter() {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        let i = (num * k as f64) as usize;\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for bucket in &mut buckets {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nvoid bucketSort(float nums[], int n) {\n    int k = n / 2;                                 // Initialize k = n/2 buckets\n    int *sizes = malloc(k * sizeof(int));          // Record each bucket's size\n    float **buckets = malloc(k * sizeof(float *)); // Array of dynamic arrays (buckets)\n    // Pre-allocate sufficient space for each bucket\n    for (int i = 0; i < k; ++i) {\n        buckets[i] = (float *)malloc(n * sizeof(float));\n        sizes[i] = 0;\n    }\n    // 1. Distribute array elements into various buckets\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. Sort each bucket\n    for (int i = 0; i < k; ++i) {\n        qsort(buckets[i], sizes[i], sizeof(float), compare);\n    }\n    // 3. Merge sorted buckets\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        // Free memory\n        free(buckets[i]);\n    }\n}\n
    bucket_sort.kt
    /* Bucket sort */\nfun bucketSort(nums: FloatArray) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\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. Distribute array elements into various buckets\n    for (num in nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        val i = (num * k).toInt()\n        // Add num to bucket i\n        buckets[i].add(num)\n    }\n    // 2. Sort each bucket\n    for (bucket in buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort()\n    }\n    // 3. Traverse buckets to merge results\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
    ### Bucket sort ###\ndef bucket_sort(nums)\n  # Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n  k = nums.length / 2\n  buckets = Array.new(k) { [] }\n\n  # 1. Distribute array elements into various buckets\n  nums.each do |num|\n    # Input data range is [0, 1), use num * k to map to index range [0, k-1]\n    i = (num * k).to_i\n    # Add num to bucket i\n    buckets[i] << num\n  end\n\n  # 2. Sort each bucket\n  buckets.each do |bucket|\n    # Use built-in sorting function, can also replace with other sorting algorithms\n    bucket.sort!\n  end\n\n  # 3. Traverse buckets to merge results\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":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182-algorithm-characteristics","level":2,"title":"11.8.2   Algorithm Characteristics","text":"

    Bucket sort is suitable for processing very large datasets. For example, suppose the input contains 1 million elements, and limited memory prevents the system from loading all of them at once. In that case, the data can be divided into 1000 buckets, each bucket can be sorted separately, and the results can then be merged.

    • Time complexity is \\(O(n + k)\\): Assuming the elements are evenly distributed across the buckets, each bucket contains \\(\\frac{n}{k}\\) elements. If sorting a single bucket takes \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) time, then sorting all buckets takes \\(O(n \\log\\frac{n}{k})\\) time. When the number of buckets \\(k\\) is relatively large, the time complexity approaches \\(O(n)\\). Merging the results requires traversing all buckets and elements, which takes \\(O(n + k)\\) time. In the worst case, all data is placed into a single bucket, and sorting that bucket takes \\(O(n^2)\\) time.
    • Space complexity is \\(O(n + k)\\), and bucket sort is not in-place: It requires extra space for \\(k\\) buckets and a total of \\(n\\) elements.
    • Whether bucket sort is stable depends on whether the algorithm for sorting elements within buckets is stable.
    ","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183-how-to-achieve-even-distribution","level":2,"title":"11.8.3   How to Achieve Even Distribution","text":"

    In theory, bucket sort can achieve \\(O(n)\\) time complexity. The key is to distribute the elements evenly across the buckets, because real-world data is often not uniformly distributed. For example, suppose we want to divide all products on Taobao evenly into 10 buckets by price range, but the price distribution is uneven: there are many products priced below 100 yuan and very few priced above 1000 yuan. If the price range is divided evenly into 10 intervals, the numbers of products in the buckets will differ greatly.

    To achieve a more even distribution, we can first choose a rough boundary and partition the data into 3 buckets. After that, buckets containing more products can be further divided into 3 buckets until the numbers of elements in all buckets are roughly equal.

    As shown in Figure 11-14, this method essentially builds a recursion tree whose goal is to make the leaf nodes as balanced as possible. Of course, the data does not have to be split into 3 buckets in every round; the specific partitioning strategy can be chosen flexibly based on the characteristics of the data.

    Figure 11-14   Recursively dividing buckets

    If we know the probability distribution of product prices in advance, we can set the price boundaries for each bucket according to that distribution. Notably, the data distribution does not need to be measured exactly; it can also be approximated with a probability model chosen to fit the characteristics of the data.

    As shown in Figure 11-15, we assume that product prices follow a normal distribution, which allows us to reasonably set price intervals to evenly distribute products to each bucket.

    Figure 11-15   Dividing buckets based on probability distribution

    ","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   Counting Sort","text":"

    Counting sort sorts by counting the occurrences of elements and is typically applied to integer arrays.

    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191-simple-implementation","level":2,"title":"11.9.1   Simple Implementation","text":"

    Let's start with a simple example. Given an array nums of length \\(n\\), where the elements are all \"non-negative integers\", the overall flow of counting sort is shown in Figure 11-16.

    1. Traverse the array to find the largest number, denoted as \\(m\\), and then create an auxiliary array counter of length \\(m + 1\\).
    2. Use counter to count how many times each number appears in nums, where counter[num] stores the number of occurrences of num. This is simple: traverse nums (denote the current number by num) and increment counter[num] by \\(1\\) each time.
    3. Because the indices of counter are naturally ordered, the numbers are effectively already sorted. Next, traverse counter and write the numbers back into nums in ascending order according to their occurrence counts.

    Figure 11-16   Counting sort flow

    The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
    def counting_sort_naive(nums: list[int]):\n    \"\"\"Counting sort\"\"\"\n    # Simple implementation, cannot be used for sorting objects\n    # 1. Count the maximum element m in the array\n    m = 0\n    for num in nums:\n        m = max(m, num)\n    # 2. Count the occurrence of each number\n    # counter[num] represents the occurrence of num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(vector<int> &nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid CountingSortNaive(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunc countingSortNaive(nums []int) {\n    // 1. Count the maximum element m in the array\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunc countingSortNaive(nums: inout [Int]) {\n    // 1. Count the maximum element m in the array\n    let m = nums.max()!\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunction countingSortNaive(nums) {\n    // 1. Count the maximum element m in the array\n    let m = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunction countingSortNaive(nums: number[]): void {\n    // 1. Count the maximum element m in the array\n    let m: number = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(List<int> nums) {\n  // 1. Count the maximum element m in the array\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Count the occurrence of each number\n  // counter[_num] represents occurrence count of _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfn counting_sort_naive(nums: &mut [i32]) {\n    // 1. Count the maximum element m in the array\n    let m = *nums.iter().max().unwrap();\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of 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. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(int nums[], int size) {\n    // 1. Count the maximum element m in the array\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. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int *counter = calloc(m + 1, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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. Free memory\n    free(counter);\n}\n
    counting_sort.kt
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfun countingSortNaive(nums: IntArray) {\n    // 1. Count the maximum element m in the array\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    ### Counting sort ###\ndef counting_sort_naive(nums)\n  # Simple implementation, cannot be used for sorting objects\n  # 1. Count the maximum element m in the array\n  m = 0\n  nums.each { |num| m = [m, num].max }\n  # 2. Count the occurrence of each number\n  # counter[num] represents the occurrence of num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Traverse counter, filling each element back into the original array 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

    Connection between counting sort and bucket sort

    From the perspective of bucket sort, each index of the counting array counter can be viewed as a bucket, and the counting process can be seen as distributing elements into their corresponding buckets. Essentially, counting sort is a special case of bucket sort for integer data.

    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192-complete-implementation","level":2,"title":"11.9.2   Complete Implementation","text":"

    Observant readers may have noticed that if the input consists of objects, step 3. above no longer works. Suppose the input consists of product objects and we want to sort them by price (a member variable of the class); the above algorithm can only produce the sorted order of the prices themselves.

    So how can we obtain the sorted order of the original data? We first compute the prefix sums of counter. As the name suggests, the prefix sum at index i, prefix[i], equals the sum of the elements from index 0 through i:

    \\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\]

    The prefix sum has a clear interpretation: prefix[num] - 1 gives the index of the last occurrence of element num in the result array res. This information is crucial because it tells us where each element should be placed in the result array. Next, we traverse the original array nums in reverse, and for each element num, perform the following two steps.

    1. Place num at index prefix[num] - 1 of the array res.
    2. Decrease the prefix sum prefix[num] by \\(1\\) to get the index for the next placement of num.

    After the traversal is complete, the array res contains the sorted result, and finally res is used to overwrite the original array nums. The complete counting sort flow is shown in Figure 11-17.

    <1><2><3><4><5><6><7><8>

    Figure 11-17   Counting sort steps

    The counting sort implementation is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
    def counting_sort(nums: list[int]):\n    \"\"\"Counting sort\"\"\"\n    # Complete implementation, can sort objects and is a stable sort\n    # 1. Count the maximum element m in the array\n    m = max(nums)\n    # 2. Count the occurrence of each number\n    # counter[num] represents the occurrence of num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    # counter[num]-1 is the last index where num appears in res\n    for i in range(m):\n        counter[i + 1] += counter[i]\n    # 4. Traverse nums in reverse order, placing each element into the result array res\n    # Initialize the array res to record results\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  # Place num at the corresponding index\n        counter[num] -= 1  # Decrement the prefix sum by 1, getting the next index to place num\n    # Use result array res to overwrite the original array nums\n    for i in range(n):\n        nums[i] = res[i]\n
    counting_sort.cpp
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(vector<int> &nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--;              // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    nums = res;\n}\n
    counting_sort.java
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
    counting_sort.cs
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid CountingSort(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
    counting_sort.go
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunc countingSort(nums []int) {\n    // 1. Count the maximum element m in the array\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i := 0; i < m; i++ {\n        counter[i+1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    n := len(nums)\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        num := nums[i]\n        // Place num at the corresponding index\n        res[counter[num]-1] = num\n        // Decrement the prefix sum by 1, getting the next index to place num\n        counter[num]--\n    }\n    // Use result array res to overwrite the original array nums\n    copy(nums, res)\n}\n
    counting_sort.swift
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunc countingSort(nums: inout [Int]) {\n    // 1. Count the maximum element m in the array\n    let m = nums.max()!\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i in 0 ..< m {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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 // Place num at the corresponding index\n        counter[num] -= 1 // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n
    counting_sort.js
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunction countingSort(nums) {\n    // 1. Count the maximum element m in the array\n    let m = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
    counting_sort.ts
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunction countingSort(nums: number[]): void {\n    // 1. Count the maximum element m in the array\n    let m: number = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
    counting_sort.dart
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(List<int> nums) {\n  // 1. Count the maximum element m in the array\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Count the occurrence of each number\n  // counter[_num] represents occurrence count of _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n  // That is, counter[_num]-1 is the last occurrence index of _num in res\n  for (int i = 0; i < m; i++) {\n    counter[i + 1] += counter[i];\n  }\n  // 4. Traverse nums in reverse order, placing each element into the result array res\n  // Initialize the array res to record results\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; // Place _num at corresponding index\n    counter[_num]--; // Decrement prefix sum by 1 to get next placement index for _num\n  }\n  // Use result array res to overwrite the original array nums\n  nums.setAll(0, res);\n}\n
    counting_sort.rs
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfn counting_sort(nums: &mut [i32]) {\n    // 1. Count the maximum element m in the array\n    let m = *nums.iter().max().unwrap() as usize;\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    let mut counter = vec![0; m + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i in 0..m {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num as usize] -= 1; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    nums.copy_from_slice(&res)\n}\n
    counting_sort.c
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(int nums[], int size) {\n    // 1. Count the maximum element m in the array\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. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int *counter = calloc(m, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--;              // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    memcpy(nums, res, size * sizeof(int));\n    // 5. Free memory\n    free(res);\n    free(counter);\n}\n
    counting_sort.kt
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfun countingSort(nums: IntArray) {\n    // 1. Count the maximum element m in the array\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (i in 0..<m) {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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 // Place num at the corresponding index\n        counter[num]-- // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (i in 0..<n) {\n        nums[i] = res[i]\n    }\n}\n
    counting_sort.rb
    ### Counting sort ###\ndef counting_sort(nums)\n  # Complete implementation, can sort objects and is a stable sort\n  # 1. Count the maximum element m in the array\n  m = nums.max\n  # 2. Count the occurrence of each number\n  # counter[num] represents the occurrence of num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n  # counter[num]-1 is the last index where num appears in res\n  (0...m).each { |i| counter[i + 1] += counter[i] }\n  # 4. Traverse nums in reverse, fill elements into result array res\n  # Initialize the array res to record results\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 # Place num at the corresponding index\n    counter[num] -= 1 # Decrement the prefix sum by 1, getting the next index to place num\n  end\n  # Use result array res to overwrite the original array nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n
    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193-algorithm-characteristics","level":2,"title":"11.9.3   Algorithm Characteristics","text":"
    • Time complexity is \\(O(n + m)\\), and counting sort is non-adaptive: Traversing nums and counter both takes linear time. In general, when \\(n \\gg m\\), the time complexity approaches \\(O(n)\\).
    • Space complexity of \\(O(n + m)\\), non-in-place sorting: Uses arrays res and counter of lengths \\(n\\) and \\(m\\) respectively.
    • Stable sorting: Since elements are filled into res in a \"right-to-left\" order, traversing nums in reverse can avoid changing the relative positions of equal elements, thereby achieving stable sorting. In fact, traversing nums in forward order can also yield correct sorting results, but the result would be unstable.
    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194-limitations","level":2,"title":"11.9.4   Limitations","text":"

    At this point, you might think counting sort is quite ingenious because it achieves efficient sorting simply by counting occurrences. However, the prerequisites for using counting sort are fairly restrictive.

    Counting sort is only applicable to non-negative integers. To apply it to other types of data, you must ensure that they can be converted to non-negative integers without changing the relative ordering of the elements. For example, for an integer array containing negative numbers, you can first add a constant to every number to shift them into the non-negative range, and then shift them back after sorting.

    Counting sort is well suited to cases with many elements but a small value range. For example, in the above scenario, \\(m\\) cannot be too large; otherwise, it consumes too much space. And when \\(n \\ll m\\), counting sort takes \\(O(m)\\) time, which may be slower than sorting algorithms with \\(O(n \\log n)\\) time complexity.

    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   Heap Sort","text":"

    Tip

    Before reading this section, please ensure you have completed the \"Heap\" chapter.

    Heap sort is an efficient sorting algorithm based on the heap data structure. We can implement heap sort using the heap construction and element removal operations introduced earlier.

    1. Input the array and build a min-heap, at which point the smallest element is at the heap top.
    2. Continuously perform element removal operations and record the removed elements in order to obtain a sequence sorted in ascending order.

    Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method.

    ","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171-algorithm-flow","level":2,"title":"11.7.1   Algorithm Flow","text":"

    Assume the array length is \\(n\\). The flow of heap sort is shown in Figure 11-12.

    1. Input the array and build a max-heap. After completion, the largest element is at the heap top.
    2. Swap the heap top element (first element) with the heap bottom element (last element). After the swap is complete, reduce the heap length by \\(1\\) and increase the count of sorted elements by \\(1\\).
    3. Starting from the heap top element, perform a top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored.
    4. Repeat steps 2. and 3. After \\(n - 1\\) rounds, the array is sorted.

    Tip

    In fact, the element removal operation also includes steps 2. and 3., with the additional step of removing the element.

    <1><2><3><4><5><6><7><8><9><10><11><12>

    Figure 11-12   Heap sort steps

    In the code below, we use the same sift_down() function for top-to-bottom heapify as in the \"Heap\" chapter. It is worth noting that since the heap length decreases as the largest element is extracted, we need to add a length parameter \\(n\\) to sift_down() to specify the current effective length of the heap. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py
    def sift_down(nums: list[int], n: int, i: int):\n    \"\"\"Heap length is n, start heapifying node i, from top to bottom\"\"\"\n    while True:\n        # Determine the largest node among i, l, r, noted as 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        # If node i is the largest or indices l, r are out of bounds, no further heapification needed, break\n        if ma == i:\n            break\n        # Swap two nodes\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # Loop downwards heapification\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"Heap sort\"\"\"\n    # Build heap operation: heapify all nodes except leaves\n    for i in range(len(nums) // 2 - 1, -1, -1):\n        sift_down(nums, len(nums), i)\n    # Extract the largest element from the heap and repeat for n-1 rounds\n    for i in range(len(nums) - 1, 0, -1):\n        # Swap the root node with the rightmost leaf node (swap the first element with the last element)\n        nums[0], nums[i] = nums[i], nums[0]\n        # Start heapifying the root node, from top to bottom\n        sift_down(nums, i, 0)\n
    heap_sort.cpp
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(vector<int> &nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i) {\n            break;\n        }\n        // Swap two nodes\n        swap(nums[i], nums[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(vector<int> &nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n        siftDown(nums, nums.size(), i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.size() - 1; i > 0; --i) {\n        // Delete node\n        swap(nums[0], nums[i]);\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.java
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(int[] nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(int[] nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.length / 2 - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.cs
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid SiftDown(int[] nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid HeapSort(int[] nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.Length / 2 - 1; i >= 0; i--) {\n        SiftDown(nums, nums.Length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Delete node\n        (nums[i], nums[0]) = (nums[0], nums[i]);\n        // Start heapifying the root node, from top to bottom\n        SiftDown(nums, i, 0);\n    }\n}\n
    heap_sort.go
    /* Heap length is n, start heapifying node i, from top to bottom */\nfunc siftDown(nums *[]int, n, i int) {\n    for true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfunc heapSort(nums *[]int) {\n    // Build heap operation: heapify all nodes except leaves\n    for i := len(*nums)/2 - 1; i >= 0; i-- {\n        siftDown(nums, len(*nums), i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i := len(*nums) - 1; i > 0; i-- {\n        // Delete node\n        (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0)\n    }\n}\n
    heap_sort.swift
    /* Heap length is n, start heapifying node i, from top to bottom */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n    var i = i\n    while true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        nums.swapAt(i, ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfunc heapSort(nums: inout [Int]) {\n    // Build heap operation: heapify all nodes except leaves\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    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i in nums.indices.dropFirst().reversed() {\n        // Delete node\n        nums.swapAt(0, i)\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums: &nums, n: i, i: 0)\n    }\n}\n
    heap_sort.js
    /* Heap length is n, start heapifying node i, from top to bottom */\nfunction siftDown(nums, n, i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma === i) {\n            break;\n        }\n        // Swap two nodes\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfunction heapSort(nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.ts
    /* Heap length is n, start heapifying node i, from top to bottom */\nfunction siftDown(nums: number[], n: number, i: number): void {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma === i) {\n            break;\n        }\n        // Swap two nodes\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfunction heapSort(nums: number[]): void {\n    // Build heap operation: heapify all nodes except leaves\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.dart
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(List<int> nums, int n, int i) {\n  while (true) {\n    // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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    // Swap two nodes\n    if (ma == i) break;\n    // Swap two nodes\n    int temp = nums[i];\n    nums[i] = nums[ma];\n    nums[ma] = temp;\n    // Loop downwards heapification\n    i = ma;\n  }\n}\n\n/* Heap sort */\nvoid heapSort(List<int> nums) {\n  // Build heap operation: heapify all nodes except leaves\n  for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {\n    siftDown(nums, nums.length, i);\n  }\n  // Extract the largest element from the heap and repeat for n-1 rounds\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Delete node\n    int tmp = nums[0];\n    nums[0] = nums[i];\n    nums[i] = tmp;\n    // Start heapifying the root node, from top to bottom\n    siftDown(nums, i, 0);\n  }\n}\n
    heap_sort.rs
    /* Heap length is n, start heapifying node i, from top to bottom */\nfn sift_down(nums: &mut [i32], n: usize, mut i: usize) {\n    loop {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break;\n        }\n        // Swap two nodes\n        nums.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfn heap_sort(nums: &mut [i32]) {\n    // Build heap operation: heapify all nodes except leaves\n    for i in (0..nums.len() / 2).rev() {\n        sift_down(nums, nums.len(), i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i in (1..nums.len()).rev() {\n        // Delete node\n        nums.swap(0, i);\n        // Start heapifying the root node, from top to bottom\n        sift_down(nums, i, 0);\n    }\n}\n
    heap_sort.c
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(int nums[], int n, int i) {\n    while (1) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i) {\n            break;\n        }\n        // Swap two nodes\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(int nums[], int n) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = n / 2 - 1; i >= 0; --i) {\n        siftDown(nums, n, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = n - 1; i > 0; --i) {\n        // Delete node\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.kt
    /* Heap length is n, start heapifying node i, from top to bottom */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n    var i = li\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i) \n            break\n        // Swap two nodes\n        val temp = nums[i]\n        nums[i] = nums[ma]\n        nums[ma] = temp\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfun heapSort(nums: IntArray) {\n    // Build heap operation: heapify all nodes except leaves\n    for (i in nums.size / 2 - 1 downTo 0) {\n        siftDown(nums, nums.size, i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (i in nums.size - 1 downTo 1) {\n        // Delete node\n        val temp = nums[0]\n        nums[0] = nums[i]\n        nums[i] = temp\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0)\n    }\n}\n
    heap_sort.rb
    ### Heap length is n, heapify from node i, top to bottom ###\ndef sift_down(nums, n, i)\n  while true\n    # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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    # Swap two nodes\n    break if ma == i\n    # Swap two nodes\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # Loop downwards heapification\n    i = ma\n  end\nend\n\n### Heap sort ###\ndef heap_sort(nums)\n  # Build heap operation: heapify all nodes except leaves\n  (nums.length / 2 - 1).downto(0) do |i|\n    sift_down(nums, nums.length, i)\n  end\n  # Extract the largest element from the heap and repeat for n-1 rounds\n  (nums.length - 1).downto(1) do |i|\n    # Delete node\n    nums[0], nums[i] = nums[i], nums[0]\n    # Start heapifying the root node, from top to bottom\n    sift_down(nums, i, 0)\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172-algorithm-characteristics","level":2,"title":"11.7.2   Algorithm Characteristics","text":"
    • Time complexity is \\(O(n \\log n)\\); heap sort is non-adaptive: Heap construction takes \\(O(n)\\) time. Extracting the largest element from the heap takes \\(O(\\log n)\\) time, and this is repeated for a total of \\(n - 1\\) rounds.
    • Space complexity is \\(O(1)\\); heap sort is in-place: A few pointer variables use \\(O(1)\\) space. Element swapping and heapify are both performed on the original array.
    • Unstable sorting: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change.
    ","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   Insertion Sort","text":"

    Insertion sort is a simple sorting algorithm that works very similarly to the process of manually sorting a deck of cards.

    Specifically, we select a base element from the unsorted portion, compare it one by one with the elements in the sorted portion to its left, and insert it into the correct position.

    Figure 11-6 illustrates how an element is inserted into an array. Let the base element be base. We need to shift all elements between the target index and base one position to the right, and then assign base to the target index.

    Figure 11-6   Single insertion operation

    ","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141-algorithm-flow","level":2,"title":"11.4.1   Algorithm Flow","text":"

    The overall flow of insertion sort is shown in Figure 11-7.

    1. Initially, the first element of the array is already sorted.
    2. Select the second element of the array as base, and after inserting it into the correct position, the first 2 elements of the array are sorted.
    3. Select the third element as base, and after inserting it into the correct position, the first 3 elements of the array are sorted.
    4. And so on. In the last round, select the last element as base, and after inserting it into the correct position, all elements are sorted.

    Figure 11-7   Insertion sort flow

    Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py
    def insertion_sort(nums: list[int]):\n    \"\"\"Insertion sort\"\"\"\n    # Outer loop: sorted interval is [0, i-1]\n    for i in range(1, len(nums)):\n        base = nums[i]\n        j = i - 1\n        # Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0 and nums[j] > base:\n            nums[j + 1] = nums[j]  # Move nums[j] to the right by one position\n            j -= 1\n        nums[j + 1] = base  # Assign base to the correct position\n
    insertion_sort.cpp
    /* Insertion sort */\nvoid insertionSort(vector<int> &nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.size(); i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
    insertion_sort.java
    /* Insertion sort */\nvoid insertionSort(int[] nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.length; i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base;        // Assign base to the correct position\n    }\n}\n
    insertion_sort.cs
    /* Insertion sort */\nvoid InsertionSort(int[] nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.Length; i++) {\n        int bas = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > bas) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = bas;         // Assign base to the correct position\n    }\n}\n
    insertion_sort.go
    /* Insertion sort */\nfunc insertionSort(nums []int) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i := 1; i < len(nums); i++ {\n        base := nums[i]\n        j := i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        for j >= 0 && nums[j] > base {\n            nums[j+1] = nums[j] // Move nums[j] to the right by one position\n            j--\n        }\n        nums[j+1] = base // Assign base to the correct position\n    }\n}\n
    insertion_sort.swift
    /* Insertion sort */\nfunc insertionSort(nums: inout [Int]) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i in nums.indices.dropFirst() {\n        let base = nums[i]\n        var j = i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0, nums[j] > base {\n            nums[j + 1] = nums[j] // Move nums[j] to the right by one position\n            j -= 1\n        }\n        nums[j + 1] = base // Assign base to the correct position\n    }\n}\n
    insertion_sort.js
    /* Insertion sort */\nfunction insertionSort(nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        let base = nums[i],\n            j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
    insertion_sort.ts
    /* Insertion sort */\nfunction insertionSort(nums: number[]): void {\n    // Outer loop: sorted interval is [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        const base = nums[i];\n        let j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
    insertion_sort.dart
    /* Insertion sort */\nvoid insertionSort(List<int> nums) {\n  // Outer loop: sorted interval is [0, i-1]\n  for (int i = 1; i < nums.length; i++) {\n    int base = nums[i], j = i - 1;\n    // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n    while (j >= 0 && nums[j] > base) {\n      nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n      j--;\n    }\n    nums[j + 1] = base; // Assign base to the correct position\n  }\n}\n
    insertion_sort.rs
    /* Insertion sort */\nfn insertion_sort(nums: &mut [i32]) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i in 1..nums.len() {\n        let (base, mut j) = (nums[i], (i - 1) as i32);\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0 && nums[j as usize] > base {\n            nums[(j + 1) as usize] = nums[j as usize]; // Move nums[j] to the right by one position\n            j -= 1;\n        }\n        nums[(j + 1) as usize] = base; // Assign base to the correct position\n    }\n}\n
    insertion_sort.c
    /* Insertion sort */\nvoid insertionSort(int nums[], int size) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < size; i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            // Move nums[j] to the right by one position\n            nums[j + 1] = nums[j];\n            j--;\n        }\n        // Assign base to the correct position\n        nums[j + 1] = base;\n    }\n}\n
    insertion_sort.kt
    /* Insertion sort */\nfun insertionSort(nums: IntArray) {\n    // Outer loop: sorted elements are 1, 2, ..., n\n    for (i in nums.indices) {\n        val base = nums[i]\n        var j = i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j] // Move nums[j] to the right by one position\n            j--\n        }\n        nums[j + 1] = base        // Assign base to the correct position\n    }\n}\n
    insertion_sort.rb
    ### Insertion sort ###\ndef insertion_sort(nums)\n  n = nums.length\n  # Outer loop: sorted interval is [0, i-1]\n  for i in 1...n\n    base = nums[i]\n    j = i - 1\n    # Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n    while j >= 0 && nums[j] > base\n      nums[j + 1] = nums[j] # Move nums[j] to the right by one position\n      j -= 1\n    end\n    nums[j + 1] = base # Assign base to the correct position\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142-algorithm-characteristics","level":2,"title":"11.4.2   Algorithm Characteristics","text":"
    • Time complexity of \\(O(n^2)\\), adaptive sorting: In the worst case, the insertion operations require \\(n - 1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), and \\(1\\) iterations, respectively, summing to \\((n - 1) n / 2\\), so the time complexity is \\(O(n^2)\\). When the data is already sorted, each insertion operation terminates early. When the input array is completely sorted, insertion sort achieves its best-case time complexity of \\(O(n)\\).
    • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
    • Stable sorting: During insertion, we place elements to the right of equal elements, so their relative order is unchanged.
    ","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143-advantages-of-insertion-sort","level":2,"title":"11.4.3   Advantages of Insertion Sort","text":"

    The time complexity of insertion sort is \\(O(n^2)\\), while the time complexity of quick sort, which we will learn about next, is \\(O(n \\log n)\\). Although insertion sort has a higher time complexity, it is usually faster on small datasets.

    This conclusion is similar to the one about when linear search and binary search are applicable. Algorithms such as quick sort, with \\(O(n \\log n)\\) complexity, are divide-and-conquer sorting algorithms and often involve more primitive operations. When the dataset is small, the values of \\(n^2\\) and \\(n \\log n\\) are relatively close, so asymptotic complexity does not dominate; instead, the number of primitive operations per round becomes the deciding factor.

    In fact, the built-in sorting functions of many programming languages (such as Java) use insertion sort. The general idea is: for large arrays, use divide-and-conquer sorting algorithms such as quick sort; for short arrays, use insertion sort directly.

    Although bubble sort, selection sort, and insertion sort all have a time complexity of \\(O(n^2)\\), in actual situations, insertion sort is used significantly more frequently than bubble sort and selection sort, mainly for the following reasons.

    • Bubble sort is implemented through element swaps, which require a temporary variable and involve 3 primitive operations; insertion sort is implemented through element assignment and requires only 1 primitive operation. Therefore, bubble sort usually has higher computational overhead than insertion sort.
    • Selection sort has a time complexity of \\(O(n^2)\\) in any case. If given a set of partially ordered data, insertion sort is usually more efficient than selection sort.
    • Selection sort is unstable and cannot be applied to multi-level sorting.
    ","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   Merge Sort","text":"

    Merge sort is a sorting algorithm based on a divide-and-conquer strategy, consisting of the \"divide\" and \"merge\" phases shown in Figure 11-10.

    1. Divide phase: Recursively split the array at the midpoint, reducing the problem of sorting a long array to the problem of sorting shorter arrays.
    2. Merge phase: When a sub-array has length 1, stop dividing and start merging, continuously combining the shorter sorted sub-arrays on the left and right into a longer sorted array until the process is complete.

    Figure 11-10   Divide and merge phases of merge sort

    ","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161-algorithm-flow","level":2,"title":"11.6.1   Algorithm Flow","text":"

    As shown in Figure 11-11, the \"divide phase\" recursively splits the array from the midpoint into two sub-arrays from top to bottom.

    1. Calculate the array midpoint mid, recursively divide the left sub-array (interval [left, mid]) and right sub-array (interval [mid + 1, right]).
    2. Repeat step 1. recursively until a sub-array has length 1.

    The \"merge phase\" merges the left and right sub-arrays into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, so every sub-array involved in this phase is already sorted.

    <1><2><3><4><5><6><7><8><9><10>

    Figure 11-11   Merge sort steps

    The recursive order of merge sort is consistent with the post-order traversal of a binary tree.

    • Post-order traversal: First recursively traverse the left subtree, then recursively traverse the right subtree, and finally process the root node.
    • Merge sort: First recursively process the left sub-array, then recursively process the right sub-array, and finally perform the merge.

    The implementation of merge sort is shown in the code below. Note that the interval to be merged in nums is [left, right], while the corresponding interval in tmp is [0, right - left].

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py
    def merge(nums: list[int], left: int, mid: int, right: int):\n    \"\"\"Merge left subarray and right subarray\"\"\"\n    # Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    # Create a temporary array tmp to store the merged results\n    tmp = [0] * (right - left + 1)\n    # Initialize the start indices of the left and right subarrays\n    i, j, k = left, mid + 1, 0\n    # While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    # Copy the remaining elements of the left and right subarrays into the temporary array\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    # Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\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    \"\"\"Merge sort\"\"\"\n    # Termination condition\n    if left >= right:\n        return  # Terminate recursion when subarray length is 1\n    # Divide and conquer stage\n    mid = (left + right) // 2  # Calculate midpoint\n    merge_sort(nums, left, mid)  # Recursively process the left subarray\n    merge_sort(nums, mid + 1, right)  # Recursively process the right subarray\n    # Merge stage\n    merge(nums, left, mid, right)\n
    merge_sort.cpp
    /* Merge left subarray and right subarray */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    vector<int> tmp(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.size(); k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    mergeSort(nums, left, mid);      // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.java
    /* Merge left subarray and right subarray */\nvoid merge(int[] nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int[] tmp = new int[right - left + 1];\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid mergeSort(int[] nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2; // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.cs
    /* Merge left subarray and right subarray */\nvoid Merge(int[] nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int[] tmp = new int[right - left + 1];\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.Length; ++k) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid MergeSort(int[] nums, int left, int right) {\n    // Termination condition\n    if (left >= right) return;       // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    MergeSort(nums, left, mid);      // Recursively process the left subarray\n    MergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    Merge(nums, left, mid, right);\n}\n
    merge_sort.go
    /* Merge left subarray and right subarray */\nfunc merge(nums []int, left, mid, right int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    tmp := make([]int, right-left+1)\n    // Initialize the start indices of the left and right subarrays\n    i, j, k := left, mid+1, 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\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    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k := 0; k < len(tmp); k++ {\n        nums[left+k] = tmp[k]\n    }\n}\n\n/* Merge sort */\nfunc mergeSort(nums []int, left, right int) {\n    // Termination condition\n    if left >= right {\n        return\n    }\n    // Divide and conquer stage\n    mid := left + (right - left) / 2\n    mergeSort(nums, left, mid)\n    mergeSort(nums, mid+1, right)\n    // Merge stage\n    merge(nums, left, mid, right)\n}\n
    merge_sort.swift
    /* Merge left subarray and right subarray */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    var tmp = Array(repeating: 0, count: right - left + 1)\n    // Initialize the start indices of the left and right subarrays\n    var i = left, j = mid + 1, k = 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\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    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in tmp.indices {\n        nums[left + k] = tmp[k]\n    }\n}\n\n/* Merge sort */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n    // Termination condition\n    if left >= right { // Terminate recursion when subarray length is 1\n        return\n    }\n    // Divide and conquer stage\n    let mid = left + (right - left) / 2 // Calculate midpoint\n    mergeSort(nums: &nums, left: left, right: mid) // Recursively process the left subarray\n    mergeSort(nums: &nums, left: mid + 1, right: right) // Recursively process the right subarray\n    // Merge stage\n    merge(nums: &nums, left: left, mid: mid, right: right)\n}\n
    merge_sort.js
    /* Merge left subarray and right subarray */\nfunction merge(nums, left, mid, right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    const tmp = new Array(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfunction mergeSort(nums, left, right) {\n    // Termination condition\n    if (left >= right) return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.ts
    /* Merge left subarray and right subarray */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    const tmp = new Array(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n    // Termination condition\n    if (left >= right) return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.dart
    /* Merge left subarray and right subarray */\nvoid merge(List<int> nums, int left, int mid, int right) {\n  // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n  // Create a temporary array tmp to store the merged results\n  List<int> tmp = List.filled(right - left + 1, 0);\n  // Initialize the start indices of the left and right subarrays\n  int i = left, j = mid + 1, k = 0;\n  // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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  // Copy the remaining elements of the left and right subarrays into the temporary array\n  while (i <= mid) {\n    tmp[k++] = nums[i++];\n  }\n  while (j <= right) {\n    tmp[k++] = nums[j++];\n  }\n  // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n  for (k = 0; k < tmp.length; k++) {\n    nums[left + k] = tmp[k];\n  }\n}\n\n/* Merge sort */\nvoid mergeSort(List<int> nums, int left, int right) {\n  // Termination condition\n  if (left >= right) return; // Terminate recursion when subarray length is 1\n  // Divide and conquer stage\n  int mid = left + (right - left) ~/ 2; // Calculate midpoint\n  mergeSort(nums, left, mid); // Recursively process the left subarray\n  mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n  // Merge stage\n  merge(nums, left, mid, right);\n}\n
    merge_sort.rs
    /* Merge left subarray and right subarray */\nfn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    let tmp_size = right - left + 1;\n    let mut tmp = vec![0; tmp_size];\n    // Initialize the start indices of the left and right subarrays\n    let (mut i, mut j, mut k) = (left, mid + 1, 0);\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\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    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in 0..tmp_size {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfn merge_sort(nums: &mut [i32], left: usize, right: usize) {\n    // Termination condition\n    if left >= right {\n        return; // Terminate recursion when subarray length is 1\n    }\n\n    // Divide and conquer stage\n    let mid = left + (right - left) / 2; // Calculate midpoint\n    merge_sort(nums, left, mid); // Recursively process the left subarray\n    merge_sort(nums, mid + 1, right); // Recursively process the right subarray\n\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.c
    /* Merge left subarray and right subarray */\nvoid merge(int *nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int tmpSize = right - left + 1;\n    int *tmp = (int *)malloc(tmpSize * sizeof(int));\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmpSize; ++k) {\n        nums[left + k] = tmp[k];\n    }\n    // Free memory\n    free(tmp);\n}\n\n/* Merge sort */\nvoid mergeSort(int *nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    mergeSort(nums, left, mid);      // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.kt
    /* Merge left subarray and right subarray */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    val tmp = IntArray(right - left + 1)\n    // Initialize the start indices of the left and right subarrays\n    var i = left\n    var j = mid + 1\n    var k = 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++]\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++]\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (l in tmp.indices) {\n        nums[left + l] = tmp[l]\n    }\n}\n\n/* Merge sort */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n    // Termination condition\n    if (left >= right) return  // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    val mid = left + (right - left) / 2 // Calculate midpoint\n    mergeSort(nums, left, mid) // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right) // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right)\n}\n
    merge_sort.rb
    ### Merge left and right subarrays ###\ndef merge(nums, left, mid, right)\n  # Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n  # Create temporary array tmp to store merged result\n  tmp = Array.new(right - left + 1, 0)\n  # Initialize the start indices of the left and right subarrays\n  i, j, k = left, mid + 1, 0\n  # While both subarrays still have elements, compare and copy the smaller element into the temporary array\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  # Copy the remaining elements of the left and right subarrays into the temporary array\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  # Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n  (0...tmp.length).each do |k|\n    nums[left + k] = tmp[k]\n  end\nend\n\n### Merge sort ###\ndef merge_sort(nums, left, right)\n  # Termination condition\n  # Terminate recursion when subarray length is 1\n  return if left >= right\n  # Divide and conquer stage\n  mid = left + (right - left) / 2 # Calculate midpoint\n  merge_sort(nums, left, mid) # Recursively process the left subarray\n  merge_sort(nums, mid + 1, right) # Recursively process the right subarray\n  # Merge stage\n  merge(nums, left, mid, right)\nend\n
    ","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162-algorithm-characteristics","level":2,"title":"11.6.2   Algorithm Characteristics","text":"
    • Time complexity is \\(O(n \\log n)\\); merge sort is non-adaptive: The divide phase produces a recursion tree of height \\(\\log n\\), and the total number of operations performed during merging at each level is \\(n\\), so the overall time complexity is \\(O(n \\log n)\\).
    • Space complexity is \\(O(n)\\); merge sort is not in-place: The recursion depth is \\(\\log n\\), which uses \\(O(\\log n)\\) stack-frame space. The merge operation requires an auxiliary array, which uses \\(O(n)\\) additional space.
    • Stable sort: During merging, the relative order of equal elements remains unchanged.
    ","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163-linked-list-sorting","level":2,"title":"11.6.3   Linked List Sorting","text":"

    For linked lists, merge sort has significant advantages over other sorting algorithms, and it can reduce the space complexity of the sorting task to \\(O(1)\\).

    • Divide phase: Iteration can be used instead of recursion to split the linked list, thereby eliminating the stack-frame space used by recursion.
    • Merge phase: In linked lists, node insertion and deletion require only pointer updates, so the merge phase (merging two short sorted linked lists into one longer sorted linked list) does not require creating an additional linked list.

    The specific implementation details are quite complex, and interested readers can consult related materials for learning.

    ","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   Quick Sort","text":"

    Quick sort is an efficient and widely used sorting algorithm based on the divide-and-conquer strategy.

    The core operation of quick sort is \"sentinel partitioning\", whose goal is to select an element as the \"pivot\", move all elements smaller than the pivot to its left, and move all elements larger than the pivot to its right. Specifically, the process is shown in Figure 11-8.

    1. Select the leftmost element as the pivot, and initialize two pointers i and j at the two ends of the array.
    2. Enter a loop. In each round, use i (j) to find the first element larger (smaller) than the pivot, and then swap the two elements.
    3. Repeat step 2. until i and j meet, then swap the pivot into the boundary position between the two sub-arrays.
    <1><2><3><4><5><6><7><8><9>

    Figure 11-8   Sentinel partitioning steps

    After sentinel partitioning, the original array is divided into three parts: the left sub-array, the pivot, and the right sub-array, such that \"any element in the left sub-array \\(\\leq\\) the pivot \\(\\leq\\) any element in the right sub-array\". Therefore, we only need to sort the two sub-arrays next.

    Divide-and-conquer strategy of quick sort

    The essence of sentinel partitioning is to simplify the sorting problem of a longer array into the sorting problems of two shorter arrays.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Sentinel partition\"\"\"\n    # Use nums[left] as the pivot\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Search from right to left for the first element smaller than the pivot\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Search from left to right for the first element greater than the pivot\n        # Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    # Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Return the index of the pivot\n
    quick_sort.cpp
    /* Sentinel partition */\nint partition(vector<int> &nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Search from left to right for the first element greater than the pivot\n        swap(nums[i], nums[j]); // Swap these two elements\n    }\n    swap(nums[i], nums[left]);  // Swap the pivot to the boundary between the two subarrays\n    return i;                   // Return the index of the pivot\n}\n
    quick_sort.java
    /* Swap elements */\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/* Sentinel partition */\nint partition(int[] nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
    quick_sort.cs
    /* Swap elements */\nvoid Swap(int[] nums, int i, int j) {\n    (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* Sentinel partition */\nint Partition(int[] nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        Swap(nums, i, j); // Swap these two elements\n    }\n    Swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
    quick_sort.go
    /* Sentinel partition */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n    // Use nums[left] as the pivot\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Search from right to left for the first element smaller than the pivot\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Return the index of the pivot\n}\n
    quick_sort.swift
    /* Sentinel partition */\nfunc partition(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while i < j {\n        while i < j, nums[j] >= nums[left] {\n            j -= 1 // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j, nums[i] <= nums[left] {\n            i += 1 // Search from left to right for the first element greater than the pivot\n        }\n        nums.swapAt(i, j) // Swap these two elements\n    }\n    nums.swapAt(i, left) // Swap the pivot to the boundary between the two subarrays\n    return i // Return the index of the pivot\n}\n
    quick_sort.js
    /* Swap elements */\nswap(nums, i, j) {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\npartition(nums, left, right) {\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
    quick_sort.ts
    /* Swap elements */\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/* Sentinel partition */\npartition(nums: number[], left: number, right: number): number {\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
    quick_sort.dart
    /* Swap elements */\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/* Sentinel partition */\nint _partition(List<int> nums, int left, int right) {\n  // Use nums[left] as the pivot\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n    while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n    _swap(nums, i, j); // Swap these two elements\n  }\n  _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n  return i; // Return the index of the pivot\n}\n
    quick_sort.rs
    /* Sentinel partition */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Use nums[left] as the pivot\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        nums.swap(i, j); // Swap these two elements\n    }\n    nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays\n    i // Return the index of the pivot\n}\n
    quick_sort.c
    /* Swap elements */\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/* Sentinel partition */\nint partition(int nums[], int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap these two elements\n        swap(nums, i, j);\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    swap(nums, i, left);\n    // Return the index of the pivot\n    return i;\n}\n
    quick_sort.kt
    /* Swap elements */\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/* Sentinel partition */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--           // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++           // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j)  // Swap these two elements\n    }\n    swap(nums, i, left)   // Swap the pivot to the boundary between the two subarrays\n    return i              // Return the index of the pivot\n}\n
    quick_sort.rb
    ### Sentinel partition ###\ndef partition(nums, left, right)\n  # Use nums[left] as the pivot\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Search from right to left for the first element smaller than the pivot\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Search from left to right for the first element greater than the pivot\n    end\n    # Swap elements\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Swap the pivot to the boundary between the two subarrays\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Return the index of the pivot\nend\n
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151-algorithm-flow","level":2,"title":"11.5.1   Algorithm Flow","text":"

    The overall flow of quick sort is shown in Figure 11-9.

    1. First, perform one \"sentinel partitioning\" on the original array to obtain the unsorted left sub-array and right sub-array.
    2. Then, recursively perform \"sentinel partitioning\" on the left sub-array and right sub-array respectively.
    3. Continue recursively until the sub-array length is 1, at which point sorting of the entire array is complete.

    Figure 11-9   Quick sort flow

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Quick sort\"\"\"\n    # Terminate recursion when subarray length is 1\n    if left >= right:\n        return\n    # Sentinel partition\n    pivot = self.partition(nums, left, right)\n    # Recursively process the left subarray and right subarray\n    self.quick_sort(nums, left, pivot - 1)\n    self.quick_sort(nums, pivot + 1, right)\n
    quick_sort.cpp
    /* Quick sort */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.java
    /* Quick sort */\nvoid quickSort(int[] nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.cs
    /* Quick sort */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = Partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    QuickSort(nums, left, pivot - 1);\n    QuickSort(nums, pivot + 1, right);\n}\n
    quick_sort.go
    /* Quick sort */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return\n    }\n    // Sentinel partition\n    pivot := q.partition(nums, left, right)\n    // Recursively process the left subarray and right subarray\n    q.quickSort(nums, left, pivot-1)\n    q.quickSort(nums, pivot+1, right)\n}\n
    quick_sort.swift
    /* Quick sort */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return\n    }\n    // Sentinel partition\n    let pivot = partition(nums: &nums, left: left, right: right)\n    // Recursively process the left subarray and right subarray\n    quickSort(nums: &nums, left: left, right: pivot - 1)\n    quickSort(nums: &nums, left: pivot + 1, right: right)\n}\n
    quick_sort.js
    /* Quick sort */\nquickSort(nums, left, right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) return;\n    // Sentinel partition\n    const pivot = this.partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.ts
    /* Quick sort */\nquickSort(nums: number[], left: number, right: number): void {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) {\n        return;\n    }\n    // Sentinel partition\n    const pivot = this.partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.dart
    /* Quick sort */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Terminate recursion when subarray length is 1\n  if (left >= right) return;\n  // Sentinel partition\n  int pivot = _partition(nums, left, right);\n  // Recursively process the left subarray and right subarray\n  quickSort(nums, left, pivot - 1);\n  quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.rs
    /* Quick sort */\npub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return;\n    }\n    // Sentinel partition\n    let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n    // Recursively process the left subarray and right subarray\n    Self::quick_sort(left, pivot - 1, nums);\n    Self::quick_sort(pivot + 1, right, nums);\n}\n
    quick_sort.c
    /* Quick sort */\nvoid quickSort(int nums[], int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) {\n        return;\n    }\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.kt
    /* Quick sort */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) return\n    // Sentinel partition\n    val pivot = partition(nums, left, right)\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1)\n    quickSort(nums, pivot + 1, right)\n}\n
    quick_sort.rb
    ### Quick sort class ###\ndef quick_sort(nums, left, right)\n  # Recurse when subarray length is not 1\n  if left < right\n    # Sentinel partition\n    pivot = partition(nums, left, right)\n    # Recursively process the left subarray and right subarray\n    quick_sort(nums, left, pivot - 1)\n    quick_sort(nums, pivot + 1, right)\n  end\n  nums\nend\n
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152-algorithm-characteristics","level":2,"title":"11.5.2   Algorithm Characteristics","text":"
    • Time complexity of \\(O(n \\log n)\\), non-adaptive sorting: On average, sentinel partitioning produces \\(\\log n\\) recursive levels, and the total number of loop iterations across each level is \\(n\\), so the overall time complexity is \\(O(n \\log n)\\). In the worst case, each round of sentinel partitioning splits an array of length \\(n\\) into sub-arrays of lengths \\(0\\) and \\(n - 1\\). The recursion depth then reaches \\(n\\), with \\(n\\) loop iterations at each level, yielding an overall time complexity of \\(O(n^2)\\).
    • Space complexity of \\(O(n)\\), in-place sorting: In the case where the input array is completely reversed, the worst recursive depth reaches \\(n\\), using \\(O(n)\\) stack frame space. The sorting operation is performed on the original array without the aid of an additional array.
    • Unstable sorting: In the last step of sentinel partitioning, the pivot may be swapped to the right of an equal element.
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153-why-is-quick-sort-fast","level":2,"title":"11.5.3   Why Is Quick Sort Fast","text":"

    As the name suggests, quick sort has clear efficiency advantages. Although its average time complexity is the same as that of \"merge sort\" and \"heap sort\", quick sort is usually faster in practice for the following reasons.

    • The worst case is unlikely to occur: Although the worst-case time complexity of quick sort is \\(O(n^2)\\) and its performance is less predictable than that of merge sort, quick sort runs in \\(O(n \\log n)\\) time in the vast majority of cases.
    • High cache efficiency: During sentinel partitioning, the system can load the entire sub-array into cache, so accessing elements is relatively efficient. By contrast, algorithms such as \"heap sort\" require non-contiguous access to elements and therefore do not enjoy this advantage.
    • Small constant factors: Among the three algorithms above, quick sort performs the fewest comparisons, assignments, and swaps in total. This is similar to why \"insertion sort\" is faster than \"bubble sort\".
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154-pivot-optimization","level":2,"title":"11.5.4   Pivot Optimization","text":"

    Quick sort can become less time-efficient for certain inputs. Consider an extreme example in which the input array is in completely descending order. Because we choose the leftmost element as the pivot, once sentinel partitioning is complete, the pivot is swapped to the far right of the array, leaving a left sub-array of length \\(n - 1\\) and a right sub-array of length \\(0\\). If this continues recursively, each round of sentinel partitioning produces one sub-array of length \\(0\\), the divide-and-conquer strategy breaks down, and quick sort degenerates into an approximation of \"bubble sort\".

    To reduce the chance of this happening, we can optimize the pivot selection strategy used in sentinel partitioning. For example, we can choose a pivot at random. However, if we are unlucky and repeatedly pick poor pivots, performance can still be unsatisfactory.

    It should be noted that programming languages usually generate \"pseudo-random numbers\". If we construct a specific test case against a pseudo-random sequence, quick sort can still suffer degraded performance.

    To improve further, we can choose three candidate elements from the array, usually the first, last, and middle elements, and use the median of the three as the pivot. This greatly increases the chance that the pivot is \"neither too small nor too large\". We can also choose more candidate elements to further improve the robustness of the algorithm. With this method, the probability that the time complexity degrades to \\(O(n^2)\\) is significantly reduced.

    Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"Select the median of three candidate elements\"\"\"\n    l, m, r = nums[left], nums[mid], nums[right]\n    if (l <= m <= r) or (r <= m <= l):\n        return mid  # m is between l and r\n    if (m <= l <= r) or (r <= l <= m):\n        return left  # l is between m and r\n    return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Sentinel partition (median of three)\"\"\"\n    # Use nums[left] as the pivot\n    med = self.median_three(nums, left, (left + right) // 2, right)\n    # Swap the median to the array's leftmost position\n    nums[left], nums[med] = nums[med], nums[left]\n    # Use nums[left] as the pivot\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Search from right to left for the first element smaller than the pivot\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Search from left to right for the first element greater than the pivot\n        # Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    # Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Return the index of the pivot\n
    quick_sort.cpp
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partition(vector<int> &nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums[left], nums[med]);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Search from left to right for the first element greater than the pivot\n        swap(nums[i], nums[j]); // Swap these two elements\n    }\n    swap(nums[i], nums[left]);  // Swap the pivot to the boundary between the two subarrays\n    return i;                   // Return the index of the pivot\n}\n
    quick_sort.java
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partition(int[] nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
    quick_sort.cs
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint Partition(int[] nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = MedianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    Swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        Swap(nums, i, j); // Swap these two elements\n    }\n    Swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
    quick_sort.go
    /* Select the median of three candidate elements */\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 is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l is between m and r\n    }\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n    // Use nums[left] as the pivot\n    med := q.medianThree(nums, left, (left+right)/2, right)\n    // Swap the median to the array's leftmost position\n    nums[left], nums[med] = nums[med], nums[left]\n    // Use nums[left] as the pivot\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Search from right to left for the first element smaller than the pivot\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Return the index of the pivot\n}\n
    quick_sort.swift
    /* Select the median of three candidate elements */\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 is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l is between m and r\n    }\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Select the median of three candidate elements\n    let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n    // Swap the median to the array's leftmost position\n    nums.swapAt(left, med)\n    return partition(nums: &nums, left: left, right: right)\n}\n
    quick_sort.js
    /* Select the median of three candidate elements */\nmedianThree(nums, left, mid, right) {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m is between l and r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l is between m and r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Sentinel partition (median of three) */\npartition(nums, left, right) {\n    // Select the median of three candidate elements\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Swap the median to the array's leftmost position\n    this.swap(nums, left, med);\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
    quick_sort.ts
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l is between m and r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Sentinel partition (median of three) */\npartition(nums: number[], left: number, right: number): number {\n    // Select the median of three candidate elements\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Swap the median to the array's leftmost position\n    this.swap(nums, left, med);\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Search from left to right for the first element greater than the pivot\n        }\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
    quick_sort.dart
    /* Select the median of three candidate elements */\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 is between l and r\n  if ((m <= l && l <= r) || (r <= l && l <= m))\n    return left; // l is between m and r\n  return right;\n}\n\n/* Sentinel partition (median of three) */\nint _partition(List<int> nums, int left, int right) {\n  // Select the median of three candidate elements\n  int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n  // Swap the median to the array's leftmost position\n  _swap(nums, left, med);\n  // Use nums[left] as the pivot\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n    while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n    _swap(nums, i, j); // Swap these two elements\n  }\n  _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n  return i; // Return the index of the pivot\n}\n
    quick_sort.rs
    /* Select the median of three candidate elements */\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 is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left; // l is between m and r\n    }\n    right\n}\n\n/* Sentinel partition (median of three) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Select the median of three candidate elements\n    let med = Self::median_three(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    nums.swap(left, med);\n    // Use nums[left] as the pivot\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        nums.swap(i, j); // Swap these two elements\n    }\n    nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays\n    i // Return the index of the pivot\n}\n
    quick_sort.c
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partitionMedian(int nums[], int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--; // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i;            // Return the index of the pivot\n}\n
    quick_sort.kt
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((l in m..r) || (l in r..m))\n        return left // l is between m and r\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // Select the median of three candidate elements\n    val med = medianThree(nums, left, (left + right) / 2, right)\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med)\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--                      // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++                      // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j)             // Swap these two elements\n    }\n    swap(nums, i, left)              // Swap the pivot to the boundary between the two subarrays\n    return i                         // Return the index of the pivot\n}\n
    quick_sort.rb
    ### Select median of three candidate elements ###\ndef median_three(nums, left, mid, right)\n  # Select the median of three candidate elements\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m is between l and r\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l is between m and r\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n### Sentinel partition (median of three) ###\ndef partition(nums, left, right)\n  ### Use nums[left] as pivot\n  med = median_three(nums, left, (left + right) / 2, right)\n  # Swap median to leftmost position of array\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 # Search from right to left for the first element smaller than the pivot\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Search from left to right for the first element greater than the pivot\n    end\n    # Swap elements\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Swap the pivot to the boundary between the two subarrays\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Return the index of the pivot\nend\n
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155-recursive-depth-optimization","level":2,"title":"11.5.5   Recursive Depth Optimization","text":"

    Quick sort may also use more space for certain inputs. Consider a fully sorted input array. Let the length of the current sub-array in the recursion be \\(m\\). Each round of sentinel partitioning produces a left sub-array of length \\(0\\) and a right sub-array of length \\(m - 1\\), which means each recursive call reduces the problem size by only one element. The recursion tree can therefore reach a height of \\(n - 1\\), requiring \\(O(n)\\) stack-frame space.

    To prevent stack frames from accumulating, we can compare the lengths of the two sub-arrays after each round of sentinel partitioning, and recurse only on the shorter one. Because the shorter sub-array has length at most \\(n / 2\\), this method ensures that the recursion depth does not exceed \\(\\log n\\), reducing the worst-case space complexity to \\(O(\\log n)\\). The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Quick sort (recursion depth optimization)\"\"\"\n    # Terminate when subarray length is 1\n    while left < right:\n        # Sentinel partition operation\n        pivot = self.partition(nums, left, right)\n        # Perform quick sort on the shorter of the two subarrays\n        if pivot - left < right - pivot:\n            self.quick_sort(nums, left, pivot - 1)  # Recursively sort the left subarray\n            left = pivot + 1  # Remaining unsorted interval is [pivot + 1, right]\n        else:\n            self.quick_sort(nums, pivot + 1, right)  # Recursively sort the right subarray\n            right = pivot - 1  # Remaining unsorted interval is [left, pivot - 1]\n
    quick_sort.cpp
    /* Quick sort (recursion depth optimization) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1;                 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1;                 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.java
    /* Quick sort (recursion depth optimization) */\nvoid quickSort(int[] nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.cs
    /* Quick sort (recursion depth optimization) */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = Partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            QuickSort(nums, left, pivot - 1);  // Recursively sort the left subarray\n            left = pivot + 1;  // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            QuickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.go
    /* Quick sort (recursion depth optimization) */\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n    // Terminate when subarray length is 1\n    for left < right {\n        // Sentinel partition operation\n        pivot := q.partition(nums, left, right)\n        // Perform quick sort on the shorter of the two subarrays\n        if pivot-left < right-pivot {\n            q.quickSort(nums, left, pivot-1) // Recursively sort the left subarray\n            left = pivot + 1                 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            q.quickSort(nums, pivot+1, right) // Recursively sort the right subarray\n            right = pivot - 1                 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.swift
    /* Quick sort (recursion depth optimization) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n    var left = left\n    var right = right\n    // Terminate when subarray length is 1\n    while left < right {\n        // Sentinel partition operation\n        let pivot = partition(nums: &nums, left: left, right: right)\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left) < (right - pivot) {\n            quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // Recursively sort the left subarray\n            left = pivot + 1 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // Recursively sort the right subarray\n            right = pivot - 1 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.js
    /* Quick sort (recursion depth optimization) */\nquickSort(nums, left, right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        let pivot = this.partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.ts
    /* Quick sort (recursion depth optimization) */\nquickSort(nums: number[], left: number, right: number): void {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        let pivot = this.partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.dart
    /* Quick sort (recursion depth optimization) */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Terminate when subarray length is 1\n  while (left < right) {\n    // Sentinel partition operation\n    int pivot = _partition(nums, left, right);\n    // Perform quick sort on the shorter of the two subarrays\n    if (pivot - left < right - pivot) {\n      quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n      left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n    } else {\n      quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n      right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n    }\n  }\n}\n
    quick_sort.rs
    /* Quick sort (recursion depth optimization) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {\n    // Terminate when subarray length is 1\n    while left < right {\n        // Sentinel partition operation\n        let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n        // Perform quick sort on the shorter of the two subarrays\n        if pivot - left < right - pivot {\n            Self::quick_sort(left, pivot - 1, nums); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            Self::quick_sort(pivot + 1, right, nums); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.c
    /* Quick sort (recursion depth optimization) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            // Recursively sort the left subarray\n            quickSortTailCall(nums, left, pivot - 1);\n            // Remaining unsorted interval is [pivot + 1, right]\n            left = pivot + 1;\n        } else {\n            // Recursively sort the right subarray\n            quickSortTailCall(nums, pivot + 1, right);\n            // Remaining unsorted interval is [left, pivot - 1]\n            right = pivot - 1;\n        }\n    }\n}\n
    quick_sort.kt
    /* Quick sort (recursion depth optimization) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n    // Terminate when subarray length is 1\n    var l = left\n    var r = right\n    while (l < r) {\n        // Sentinel partition operation\n        val pivot = partition(nums, l, r)\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - l < r - pivot) {\n            quickSort(nums, l, pivot - 1) // Recursively sort the left subarray\n            l = pivot + 1 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, r) // Recursively sort the right subarray\n            r = pivot - 1 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.rb
    ### Quick sort (recursion depth optimization) ###\ndef quick_sort(nums, left, right)\n  # Recurse when subarray length is not 1\n  while left < right\n    # Sentinel partition\n    pivot = partition(nums, left, right)\n    # Perform quick sort on the shorter of the two subarrays\n    if pivot - left < right - pivot\n      quick_sort(nums, left, pivot - 1)\n      left = pivot + 1 # Remaining unsorted interval is [pivot + 1, right]\n    else\n      quick_sort(nums, pivot + 1, right)\n      right = pivot - 1 # Remaining unsorted interval is [left, pivot - 1]\n    end\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   Radix Sort","text":"

    The previous section introduced counting sort, which is suitable when the number of items \\(n\\) is large but the value range \\(m\\) is small. Suppose we need to sort \\(n = 10^6\\) student IDs, each of which is an 8-digit number. Then the value range \\(m = 10^8\\) is very large. Using counting sort would require a large amount of memory, whereas radix sort avoids this problem.

    Radix sort is based on the same core idea as counting sort: it also sorts by counting occurrences. Building on this, radix sort exploits the positional relationship among digits and sorts them one digit at a time to obtain the final result.

    ","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101-algorithm-flow","level":2,"title":"11.10.1   Algorithm Flow","text":"

    Taking student ID data as an example, assume the lowest digit is the \\(1\\)st digit and the highest digit is the \\(8\\)th digit. The flow of radix sort is shown in Figure 11-18.

    1. Initialize the digit \\(k = 1\\).
    2. Perform \"counting sort\" on the \\(k\\)th digit of the student IDs. After completion, the data will be sorted from smallest to largest according to the \\(k\\)th digit.
    3. Increase \\(k\\) by \\(1\\), then return to step 2. and continue iterating until all digits are sorted, at which point the process ends.

    Figure 11-18   Radix sort algorithm flow

    Next, let us look at the code. For a number \\(x\\) in base \\(d\\), its \\(k\\)th digit \\(x_k\\) can be obtained with the following formula:

    \\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\]

    Here, \\(\\lfloor a \\rfloor\\) denotes rounding the floating-point number \\(a\\) down, and \\(\\bmod \\: d\\) denotes taking the remainder modulo \\(d\\). For student ID data, \\(d = 10\\) and \\(k \\in [1, 8]\\).

    Additionally, we need to slightly modify the counting sort code to make it sort based on the \\(k\\)th digit of the number:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py
    def digit(num: int, exp: int) -> int:\n    \"\"\"Get the k-th digit of element num, where exp = 10^(k-1)\"\"\"\n    # Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"Counting sort (based on nums k-th digit)\"\"\"\n    # Decimal digit range is 0~9, therefore need a bucket array of length 10\n    counter = [0] * 10\n    n = len(nums)\n    # Count the occurrence of digits 0~9\n    for i in range(n):\n        d = digit(nums[i], exp)  # Get the k-th digit of nums[i], noted as d\n        counter[d] += 1  # Count the occurrence of digit d\n    # Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in range(1, 10):\n        counter[i] += counter[i - 1]\n    # Traverse in reverse, based on bucket statistics, place each element into 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  # Get the index j for d in the array\n        res[j] = nums[i]  # Place the current element at index j\n        counter[d] -= 1  # Decrease the count of d by 1\n    # Use result to overwrite the original array nums\n    for i in range(n):\n        nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n    \"\"\"Radix sort\"\"\"\n    # Get the maximum element of the array, used to determine the maximum number of digits\n    m = max(nums)\n    # Traverse from the lowest to the highest digit\n    exp = 1\n    while exp <= m:\n        # Perform counting sort on the k-th digit of array elements\n        # k = 1 -> exp = 1\n        # k = 2 -> exp = 10\n        # i.e., exp = 10^(k-1)\n        counting_sort_digit(nums, exp)\n        exp *= 10\n
    radix_sort.cpp
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    vector<int> counter(10, 0);\n    int n = nums.size();\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(vector<int> &nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = *max_element(nums.begin(), nums.end());\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10)\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n}\n
    radix_sort.java
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int[] counter = new int[10];\n    int n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(int[] nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = Integer.MIN_VALUE;\n    for (int num : nums)\n        if (num > m)\n            m = num;\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
    radix_sort.cs
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int[] counter = new int[10];\n    int n = nums.Length;\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = Digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nvoid RadixSort(int[] nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = int.MinValue;\n    foreach (int num in nums) {\n        if (num > m) m = num;\n    }\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        CountingSortDigit(nums, exp);\n    }\n}\n
    radix_sort.go
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunc countingSortDigit(nums []int, exp int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    counter := make([]int, 10)\n    n := len(nums)\n    // Count the occurrence of digits 0~9\n    for i := 0; i < n; i++ {\n        d := digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d\n        counter[d]++             // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i := 1; i < 10; i++ {\n        counter[i] += counter[i-1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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 // Get the index j for d in the array\n        res[j] = nums[i]    // Place the current element at index j\n        counter[d]--        // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for i := 0; i < n; i++ {\n        nums[i] = res[i]\n    }\n}\n\n/* Radix sort */\nfunc radixSort(nums []int) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    max := math.MinInt\n    for _, num := range nums {\n        if num > max {\n            max = num\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for exp := 1; max >= exp; exp *= 10 {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n    }\n}\n
    radix_sort.swift
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    var counter = Array(repeating: 0, count: 10)\n    // Count the occurrence of digits 0~9\n    for i in nums.indices {\n        let d = digit(num: nums[i], exp: exp) // Get the k-th digit of nums[i], noted as d\n        counter[d] += 1 // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in 1 ..< 10 {\n        counter[i] += counter[i - 1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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 // Get the index j for d in the array\n        res[j] = nums[i] // Place the current element at index j\n        counter[d] -= 1 // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n\n/* Radix sort */\nfunc radixSort(nums: inout [Int]) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    var m = Int.min\n    for num in nums {\n        if num > m {\n            m = num\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums: &nums, exp: exp)\n    }\n}\n
    radix_sort.js
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return Math.floor(num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunction countingSortDigit(nums, exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d]--; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nfunction radixSort(nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m = Math.max(... nums);\n    // Traverse from the lowest to the highest digit\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
    radix_sort.ts
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return Math.floor(num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d]--; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nfunction radixSort(nums: number[]): void {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m: number = Math.max(... nums);\n    // Traverse from the lowest to the highest digit\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
    radix_sort.dart
    /* Get k-th digit of element _num, where exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // Passing exp instead of k can avoid repeated expensive exponentiation here\n  return (_num ~/ exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // Decimal digit range is 0~9, therefore need a bucket array of length 10\n  List<int> counter = List<int>.filled(10, 0);\n  int n = nums.length;\n  // Count the occurrence of digits 0~9\n  for (int i = 0; i < n; i++) {\n    int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n    counter[d]++; // Count the occurrence of digit d\n  }\n  // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n  for (int i = 1; i < 10; i++) {\n    counter[i] += counter[i - 1];\n  }\n  // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n    res[j] = nums[i]; // Place the current element at index j\n    counter[d]--; // Decrease the count of d by 1\n  }\n  // Use result to overwrite the original array nums\n  for (int i = 0; i < n; i++) nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(List<int> nums) {\n  // Get the maximum element of the array, used to determine the maximum number of digits\n  // In Dart, int length is 64 bits\n  int m = -1 << 63;\n  for (int _num in nums) if (_num > m) m = _num;\n  // Traverse from the lowest to the highest digit\n  for (int exp = 1; exp <= m; exp *= 10)\n    // Perform counting sort on the k-th digit of array elements\n    // k = 1 -> exp = 1\n    // k = 2 -> exp = 10\n    // i.e., exp = 10^(k-1)\n    countingSortDigit(nums, exp);\n}\n
    radix_sort.rs
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return ((num / exp) % 10) as usize;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    let mut counter = [0; 10];\n    let n = nums.len();\n    // Count the occurrence of digits 0~9\n    for i in 0..n {\n        let d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d] += 1; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in 1..10 {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d] -= 1; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    nums.copy_from_slice(&res);\n}\n\n/* Radix sort */\nfn radix_sort(nums: &mut [i32]) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m = *nums.into_iter().max().unwrap();\n    // Traverse from the lowest to the highest digit\n    let mut exp = 1;\n    while exp <= m {\n        counting_sort_digit(nums, exp);\n        exp *= 10;\n    }\n}\n
    radix_sort.c
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // Initialize to 0 to support subsequent memory release\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < size; i++) {\n        // Get the k-th digit of nums[i], noted as d\n        int d = digit(nums[i], exp);\n        // Count the occurrence of digit d\n        counter[d]++;\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < size; i++) {\n        nums[i] = res[i];\n    }\n    // Free memory\n    free(res);\n    free(counter);\n}\n\n/* Radix sort */\nvoid radixSort(int nums[], int size) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\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    // Traverse from the lowest to the highest digit\n    for (int exp = 1; max >= exp; exp *= 10)\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, size, exp);\n}\n
    radix_sort.kt
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    val counter = IntArray(10)\n    val n = nums.size\n    // Count the occurrence of digits 0~9\n    for (i in 0..<n) {\n        val d = digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d\n        counter[d]++                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (i in 1..9) {\n        counter[i] += counter[i - 1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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 // Get the index j for d in the array\n        res[j] = nums[i]       // Place the current element at index j\n        counter[d]--           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (i in 0..<n)\n        nums[i] = res[i]\n}\n\n/* Radix sort */\nfun radixSort(nums: IntArray) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    var m = Int.MIN_VALUE\n    for (num in nums) if (num > m) m = num\n    var exp = 1\n    // Traverse from the lowest to the highest digit\n    while (exp <= m) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n        exp *= 10\n    }\n}\n
    radix_sort.rb
    ### Get k-th digit of element num, where exp = 10^(k-1) ###\ndef digit(num, exp)\n  # Passing exp instead of k avoids expensive exponentiation calculations\n  (num / exp) % 10\nend\n\n### Counting sort (sort by k-th digit of nums) ###\ndef counting_sort_digit(nums, exp)\n  # Decimal digit range is 0~9, therefore need a bucket array of length 10\n  counter = Array.new(10, 0)\n  n = nums.length\n  # Count the occurrence of digits 0~9\n  for i in 0...n\n    d = digit(nums[i], exp) # Get the k-th digit of nums[i], noted as d\n    counter[d] += 1 # Count the occurrence of digit d\n  end\n  # Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n  (1...10).each { |i| counter[i] += counter[i - 1] }\n  # Traverse in reverse, based on bucket statistics, place each element into 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 # Get the index j for d in the array\n    res[j] = nums[i] # Place the current element at index j\n    counter[d] -= 1 # Decrease the count of d by 1\n  end\n  # Use result to overwrite the original array nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n\n### Radix sort ###\ndef radix_sort(nums)\n  # Get the maximum element of the array, used to determine the maximum number of digits\n  m = nums.max\n  # Traverse from the lowest to the highest digit\n  exp = 1\n  while exp <= m\n    # Perform counting sort on the k-th digit of array elements\n    # k = 1 -> exp = 1\n    # k = 2 -> exp = 10\n    # i.e., exp = 10^(k-1)\n    counting_sort_digit(nums, exp)\n    exp *= 10\n  end\nend\n

    Why start sorting from the lowest digit?

    In successive sorting passes, a later pass overrides the result of an earlier one. For example, if the first pass yields \\(a < b\\) but the second yields \\(a > b\\), then the result of the second pass prevails. Because higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then the higher digits.

    ","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102-algorithm-characteristics","level":2,"title":"11.10.2   Algorithm Characteristics","text":"

    Compared with counting sort, radix sort is suitable for larger value ranges, but only when the data can be represented with a fixed number of digits and that digit count is not too large. For example, floating-point numbers are not well suited to radix sort because the digit count \\(k\\) can be too large, potentially leading to time complexity \\(O(nk) \\gg O(n^2)\\).

    • Time complexity of \\(O(nk)\\), non-adaptive sorting: Let the number of items be \\(n\\), let the values be represented in base \\(d\\), and let the maximum number of digits be \\(k\\). Counting sort on one digit takes \\(O(n + d)\\) time, so sorting all \\(k\\) digits takes \\(O((n + d)k)\\) time. In practice, \\(d\\) and \\(k\\) are usually relatively small, so the overall time complexity approaches \\(O(n)\\).
    • Space complexity of \\(O(n + d)\\), non-in-place sorting: Same as counting sort, radix sort requires auxiliary arrays res and counter of lengths \\(n\\) and \\(d\\).
    • Stable sort: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee correct sorting results.
    ","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   Selection Sort","text":"

    Selection sort works very simply: in each round, it selects the smallest element from the unsorted interval and places it at the end of the sorted interval.

    Assume the array has length \\(n\\). The procedure of selection sort is shown in Figure 11-2.

    1. Initially, all elements are unsorted, i.e., the unsorted (index) interval is \\([0, n-1]\\).
    2. Select the smallest element in the interval \\([0, n-1]\\) and swap it with the element at index \\(0\\). After completion, the first element of the array is sorted.
    3. Select the smallest element in the interval \\([1, n-1]\\) and swap it with the element at index \\(1\\). After completion, the first 2 elements of the array are sorted.
    4. And so on. After \\(n - 1\\) rounds of selection and swapping, the first \\(n - 1\\) elements of the array are sorted.
    5. The only remaining element must be the largest, so no further sorting is needed and the array is sorted.
    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 11-2   Selection sort steps

    In the code, we use \\(k\\) to track the smallest element within the unsorted interval:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py
    def selection_sort(nums: list[int]):\n    \"\"\"Selection sort\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [i, n-1]\n    for i in range(n - 1):\n        # Inner loop: find the smallest element within the unsorted interval\n        k = i\n        for j in range(i + 1, n):\n            if nums[j] < nums[k]:\n                k = j  # Record the index of the smallest element\n        # Swap the smallest element with the first element of the unsorted interval\n        nums[i], nums[k] = nums[k], nums[i]\n
    selection_sort.cpp
    /* Selection sort */\nvoid selectionSort(vector<int> &nums) {\n    int n = nums.size();\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        swap(nums[i], nums[k]);\n    }\n}\n
    selection_sort.java
    /* Selection sort */\nvoid selectionSort(int[] nums) {\n    int n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
    selection_sort.cs
    /* Selection sort */\nvoid SelectionSort(int[] nums) {\n    int n = nums.Length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        (nums[k], nums[i]) = (nums[i], nums[k]);\n    }\n}\n
    selection_sort.go
    /* Selection sort */\nfunc selectionSort(nums []int) {\n    n := len(nums)\n    // Outer loop: unsorted interval is [i, n-1]\n    for i := 0; i < n-1; i++ {\n        // Inner loop: find the smallest element within the unsorted interval\n        k := i\n        for j := i + 1; j < n; j++ {\n            if nums[j] < nums[k] {\n                // Record the index of the smallest element\n                k = j\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums[i], nums[k] = nums[k], nums[i]\n\n    }\n}\n
    selection_sort.swift
    /* Selection sort */\nfunc selectionSort(nums: inout [Int]) {\n    // Outer loop: unsorted interval is [i, n-1]\n    for i in nums.indices.dropLast() {\n        // Inner loop: find the smallest element within the unsorted interval\n        var k = i\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[j] < nums[k] {\n                k = j // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums.swapAt(i, k)\n    }\n}\n
    selection_sort.js
    /* Selection sort */\nfunction selectionSort(nums) {\n    let n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
    selection_sort.ts
    /* Selection sort */\nfunction selectionSort(nums: number[]): void {\n    let n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
    selection_sort.dart
    /* Selection sort */\nvoid selectionSort(List<int> nums) {\n  int n = nums.length;\n  // Outer loop: unsorted interval is [i, n-1]\n  for (int i = 0; i < n - 1; i++) {\n    // Inner loop: find the smallest element within the unsorted interval\n    int k = i;\n    for (int j = i + 1; j < n; j++) {\n      if (nums[j] < nums[k]) k = j; // Record the index of the smallest element\n    }\n    // Swap the smallest element with the first element of the unsorted interval\n    int temp = nums[i];\n    nums[i] = nums[k];\n    nums[k] = temp;\n  }\n}\n
    selection_sort.rs
    /* Selection sort */\nfn selection_sort(nums: &mut [i32]) {\n    if nums.is_empty() {\n        return;\n    }\n    let n = nums.len();\n    // Outer loop: unsorted interval is [i, n-1]\n    for i in 0..n - 1 {\n        // Inner loop: find the smallest element within the unsorted interval\n        let mut k = i;\n        for j in i + 1..n {\n            if nums[j] < nums[k] {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums.swap(i, k);\n    }\n}\n
    selection_sort.c
    /* Selection sort */\nvoid selectionSort(int nums[], int n) {\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
    selection_sort.kt
    /* Selection sort */\nfun selectionSort(nums: IntArray) {\n    val n = nums.size\n    // Outer loop: unsorted interval is [i, n-1]\n    for (i in 0..<n - 1) {\n        var k = i\n        // Inner loop: find the smallest element within the unsorted interval\n        for (j in i + 1..<n) {\n            if (nums[j] < nums[k])\n                k = j // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        val temp = nums[i]\n        nums[i] = nums[k]\n        nums[k] = temp\n    }\n}\n
    selection_sort.rb
    ### Selection sort ###\ndef selection_sort(nums)\n  n = nums.length\n  # Outer loop: unsorted interval is [i, n-1]\n  for i in 0...(n - 1)\n    # Inner loop: find the smallest element within the unsorted interval\n    k = i\n    for j in (i + 1)...n\n      if nums[j] < nums[k]\n        k = j # Record the index of the smallest element\n      end\n    end\n    # Swap the smallest element with the first element of the unsorted interval\n    nums[i], nums[k] = nums[k], nums[i]\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.2   Selection Sort"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121-algorithm-characteristics","level":2,"title":"11.2.1   Algorithm Characteristics","text":"
    • Time complexity \\(O(n^2)\\), non-adaptive sorting: The outer loop has \\(n - 1\\) rounds in total. The length of the unsorted interval in the first round is \\(n\\), and the length of the unsorted interval in the last round is \\(2\\). That is, the rounds of the outer loop contain inner loops with \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(3\\), and \\(2\\) iterations, summing to \\(\\frac{(n - 1)(n + 2)}{2}\\).
    • Space complexity \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
    • Unstable sorting: As shown in Figure 11-3, element nums[i] may be swapped to the right of an element equal to it, causing a change in their relative order.

    Figure 11-3   Selection sort non-stability example

    ","path":["Chapter 11. Sorting","11.2   Selection Sort"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   Sorting Algorithm","text":"

    A sorting algorithm arranges a set of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently.

    As shown in Figure 11-1, the data being sorted can be integers, floating-point numbers, characters, strings, and so on. The sorting rule can be defined as needed, such as numerical order, ASCII order, or a custom rule.

    Figure 11-1   Data type and criterion examples

    ","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111-evaluation-dimensions","level":2,"title":"11.1.1   Evaluation Dimensions","text":"

    Execution efficiency: We expect the time complexity of sorting algorithms to be as low as possible, with a smaller total number of operations (reducing the constant factor in time complexity). For large data volumes, execution efficiency is particularly important.

    In-place property: As the name implies, in-place sorting achieves sorting by operating directly on the original array without requiring additional auxiliary arrays, thus saving memory. Typically, in-place sorting involves fewer data movement operations and runs faster.

    Stability: Stable sorting ensures that the relative order of equal elements in the array does not change after sorting is completed.

    Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, unstable sorting may cause the ordered nature of the input data to be lost:

    # The input data is sorted by name\n# (name, age)\n  ('A', 19)\n  ('B', 18)\n  ('C', 21)\n  ('D', 19)\n  ('E', 23)\n\n# Suppose we use an unstable sorting algorithm to sort the list by age.\n# In the result, the relative positions of ('D', 19) and ('A', 19) change,\n# so the property that the input data is sorted by name is lost.\n  ('B', 18)\n  ('D', 19)\n  ('A', 19)\n  ('C', 21)\n  ('E', 23)\n

    Adaptability: Adaptive sorting can utilize the existing order information in the input data to reduce the amount of computation, achieving better time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than the average time complexity.

    Comparison-based or non-comparison: Comparison-based sorting relies on comparison operators (\\(<\\), \\(=\\), \\(>\\)) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of \\(O(n \\log n)\\). Non-comparison sorting does not use comparison operators and can achieve a time complexity of \\(O(n)\\), but its versatility is relatively limited.

    ","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112-ideal-sorting-algorithm","level":2,"title":"11.1.2   Ideal Sorting Algorithm","text":"

    Fast, in-place, stable, adaptive, and broadly applicable. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem.

    Next, we will examine various sorting algorithms and analyze their advantages and disadvantages based on the evaluation dimensions above.

    ","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   Summary","text":"","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_sorting/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to \\(O(n)\\).
    • In each round, insertion sort inserts an element from the unsorted portion into its correct position in the sorted portion. Although insertion sort has a time complexity of \\(O(n^2)\\), it remains very popular for small sorting tasks because each operation is relatively lightweight.
    • Quick sort relies on sentinel partitioning. In sentinel partitioning, repeatedly choosing the worst possible pivot can degrade the time complexity to \\(O(n^2)\\). Choosing a median-based pivot or a random pivot can reduce the probability of this degradation. By recursing on the shorter subarray first, we can effectively reduce the recursion depth and optimize the space complexity to \\(O(\\log n)\\).
    • Merge sort includes two phases: divide and merge, which typically embody the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, with a space complexity of \\(O(n)\\); however, the space complexity of sorting a linked list can be optimized to \\(O(1)\\).
    • Bucket sort consists of three steps: distributing data into buckets, sorting within buckets, and merging results. It also embodies the divide-and-conquer strategy and is suitable for very large data volumes. The key to bucket sort is distributing data evenly.
    • Counting sort is a special case of bucket sort, which achieves sorting by counting the number of occurrences of data. Counting sort is suitable for situations where the data volume is large but the data range is limited, and requires that data can be converted to positive integers.
    • Radix sort achieves data sorting by sorting digit by digit, requiring that data can be represented as fixed-digit numbers.
    • Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive. However, as with other data structures and algorithms, no sorting algorithm can satisfy all of these criteria at the same time. In practice, we need to choose the appropriate sorting algorithm based on the characteristics of the data.
    • Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place property, and adaptability.

    Figure 11-19   Sorting algorithm comparison

    ","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: In what situations is the stability of sorting algorithms necessary?

    In reality, we may sort based on a certain attribute of objects. For example, students have two attributes: name and height. We want to implement multi-level sorting: first sort by name to get (A, 180) (B, 185) (C, 170) (D, 170); then sort by height. Because the sorting algorithm is unstable, we may get (D, 170) (C, 170) (A, 180) (B, 185).

    We can see that students D and C have swapped positions, destroying the ordering by name, which is not what we want.

    Q: Can the order of \"searching from right to left\" and \"searching from left to right\" in sentinel partitioning be swapped?

    No. When we use the leftmost element as the pivot, we must first \"search from right to left\" and then \"search from left to right\". This conclusion is somewhat counterintuitive; let's analyze the reason.

    The last step of sentinel partitioning partition() is to swap nums[left] and nums[i]. After the swap is complete, the elements to the left of the pivot are all <= the pivot, which requires that nums[left] >= nums[i] must hold before the last swap. Suppose we first \"search from left to right\", then if we cannot find an element larger than the pivot, we will exit the loop when i == j, at which point it may be that nums[j] == nums[i] > nums[left]. In other words, the last swap operation will swap an element larger than the pivot to the leftmost end of the array, causing sentinel partitioning to fail.

    For example, given the array [0, 0, 0, 0, 1], if we first \"search from left to right\", the array after sentinel partitioning is [1, 0, 0, 0, 0], which is incorrect.

    By the same reasoning, if we select nums[right] as the pivot, the order is reversed: we must first \"search from left to right\".

    Q: Regarding the optimization of recursion depth in quick sort, why can selecting the shorter array ensure that the recursion depth does not exceed \\(\\log n\\)?

    Recursion depth is the number of recursive calls that have not yet returned. Each round of sentinel partitioning divides the original array into two sub-arrays. After this optimization, the sub-array selected for further recursion is at most half the length of the original array. In the worst case, if it is always half as long, the final recursion depth is \\(\\log n\\).

    Reviewing the original quick sort, we may continuously recurse on the longer array. In the worst case, it would be \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(2\\), \\(1\\), with a recursion depth of \\(n\\). Recursion depth optimization can avoid this situation.

    Q: When all elements in the array are equal, is the time complexity of quick sort \\(O(n^2)\\)? How should this degenerate case be handled?

    Yes. In this case, the array can be partitioned into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. We then recurse only on the less-than and greater-than parts. With this approach, an array whose elements are all equal can be sorted in just one round of sentinel partitioning.

    Q: Why is the worst-case time complexity of bucket sort \\(O(n^2)\\)?

    In the worst case, all elements are distributed into the same bucket. If we use an \\(O(n^2)\\) algorithm to sort these elements, the time complexity will be \\(O(n^2)\\).

    ","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"Chapter 5.   Stacks and Queues","text":"

    Abstract

    A stack is like cats piled on top of one another, while a queue is like cats lining up.

    They represent the logical relationships of LIFO (Last In, First Out) and FIFO (First In, First Out), respectively.

    ","path":["Chapter 5. Stacks and Queues","Chapter 5.   Stacks and Queues"],"tags":[]},{"location":"chapter_stack_and_queue/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 5.1   Stack
    • 5.2   Queue
    • 5.3   Deque
    • 5.4   Summary
    ","path":["Chapter 5. Stacks and Queues","Chapter 5.   Stacks and Queues"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   Deque","text":"

    In a queue, we can only remove elements from the front or add elements at the rear. As shown in Figure 5-7, a double-ended queue (deque) provides greater flexibility, allowing elements to be added or removed at both the front and the rear.

    Figure 5-7   Operations of deque

    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531-common-deque-operations","level":2,"title":"5.3.1   Common Deque Operations","text":"

    The common operations on a deque are shown in Table 5-3. The specific method names depend on the programming language used.

    Table 5-3   Efficiency of Deque Operations

    Method Description Time Complexity push_first() Add element to front \\(O(1)\\) push_last() Add element to rear \\(O(1)\\) pop_first() Remove front element \\(O(1)\\) pop_last() Remove rear element \\(O(1)\\) peek_first() Access front element \\(O(1)\\) peek_last() Access rear element \\(O(1)\\)

    Similarly, we can directly use the deque classes provided by the programming language:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py
    from collections import deque\n\n# Initialize deque\ndeq: deque[int] = deque()\n\n# Enqueue elements\ndeq.append(2)      # Add to rear\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3)  # Add to front\ndeq.appendleft(1)\n\n# Access elements\nfront: int = deq[0]  # Front element\nrear: int = deq[-1]  # Rear element\n\n# Dequeue elements\npop_front: int = deq.popleft()  # Front element dequeue\npop_rear: int = deq.pop()       # Rear element dequeue\n\n# Get deque length\nsize: int = len(deq)\n\n# Check if deque is empty\nis_empty: bool = len(deq) == 0\n
    deque.cpp
    /* Initialize deque */\ndeque<int> deque;\n\n/* Enqueue elements */\ndeque.push_back(2);   // Add to rear\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3);  // Add to front\ndeque.push_front(1);\n\n/* Access elements */\nint front = deque.front(); // Front element\nint back = deque.back();   // Rear element\n\n/* Dequeue elements */\ndeque.pop_front();  // Front element dequeue\ndeque.pop_back();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.size();\n\n/* Check if deque is empty */\nbool empty = deque.empty();\n
    deque.java
    /* Initialize deque */\nDeque<Integer> deque = new LinkedList<>();\n\n/* Enqueue elements */\ndeque.offerLast(2);   // Add to rear\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3);  // Add to front\ndeque.offerFirst(1);\n\n/* Access elements */\nint peekFirst = deque.peekFirst();  // Front element\nint peekLast = deque.peekLast();    // Rear element\n\n/* Dequeue elements */\nint popFirst = deque.pollFirst();  // Front element dequeue\nint popLast = deque.pollLast();    // Rear element dequeue\n\n/* Get deque length */\nint size = deque.size();\n\n/* Check if deque is empty */\nboolean isEmpty = deque.isEmpty();\n
    deque.cs
    /* Initialize deque */\n// In C#, use LinkedList as a deque\nLinkedList<int> deque = new();\n\n/* Enqueue elements */\ndeque.AddLast(2);   // Add to rear\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3);  // Add to front\ndeque.AddFirst(1);\n\n/* Access elements */\nint peekFirst = deque.First.Value;  // Front element\nint peekLast = deque.Last.Value;    // Rear element\n\n/* Dequeue elements */\ndeque.RemoveFirst();  // Front element dequeue\ndeque.RemoveLast();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.Count;\n\n/* Check if deque is empty */\nbool isEmpty = deque.Count == 0;\n
    deque_test.go
    /* Initialize deque */\n// In Go, use list as a deque\ndeque := list.New()\n\n/* Enqueue elements */\ndeque.PushBack(2)      // Add to rear\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3)     // Add to front\ndeque.PushFront(1)\n\n/* Access elements */\nfront := deque.Front() // Front element\nrear := deque.Back()   // Rear element\n\n/* Dequeue elements */\ndeque.Remove(front)    // Front element dequeue\ndeque.Remove(rear)     // Rear element dequeue\n\n/* Get deque length */\nsize := deque.Len()\n\n/* Check if deque is empty */\nisEmpty := deque.Len() == 0\n
    deque.swift
    /* Initialize deque */\n// Swift does not have a built-in deque class, can use Array as a deque\nvar deque: [Int] = []\n\n/* Enqueue elements */\ndeque.append(2) // Add to rear\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // Add to front\ndeque.insert(1, at: 0)\n\n/* Access elements */\nlet peekFirst = deque.first! // Front element\nlet peekLast = deque.last! // Rear element\n\n/* Dequeue elements */\n// When using Array simulation, popFirst has O(n) complexity\nlet popFirst = deque.removeFirst() // Front element dequeue\nlet popLast = deque.removeLast() // Rear element dequeue\n\n/* Get deque length */\nlet size = deque.count\n\n/* Check if deque is empty */\nlet isEmpty = deque.isEmpty\n
    deque.js
    /* Initialize deque */\n// JavaScript does not have a built-in deque, can only use Array as a deque\nconst deque = [];\n\n/* Enqueue elements */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Please note that since it's an array, unshift() has O(n) time complexity\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Access elements */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* Dequeue elements */\n// Please note that since it's an array, shift() has O(n) time complexity\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* Get deque length */\nconst size = deque.length;\n\n/* Check if deque is empty */\nconst isEmpty = size === 0;\n
    deque.ts
    /* Initialize deque */\n// TypeScript does not have a built-in deque, can only use Array as a deque\nconst deque: number[] = [];\n\n/* Enqueue elements */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Please note that since it's an array, unshift() has O(n) time complexity\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Access elements */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* Dequeue elements */\n// Please note that since it's an array, shift() has O(n) time complexity\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* Get deque length */\nconst size: number = deque.length;\n\n/* Check if deque is empty */\nconst isEmpty: boolean = size === 0;\n
    deque.dart
    /* Initialize deque */\n// In Dart, Queue is defined as a deque\nQueue<int> deque = Queue<int>();\n\n/* Enqueue elements */\ndeque.addLast(2);  // Add to rear\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // Add to front\ndeque.addFirst(1);\n\n/* Access elements */\nint peekFirst = deque.first; // Front element\nint peekLast = deque.last;   // Rear element\n\n/* Dequeue elements */\nint popFirst = deque.removeFirst(); // Front element dequeue\nint popLast = deque.removeLast();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.length;\n\n/* Check if deque is empty */\nbool isEmpty = deque.isEmpty;\n
    deque.rs
    /* Initialize deque */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Enqueue elements */\ndeque.push_back(2);  // Add to rear\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // Add to front\ndeque.push_front(1);\n\n/* Access elements */\nif let Some(front) = deque.front() { // Front element\n}\nif let Some(rear) = deque.back() {   // Rear element\n}\n\n/* Dequeue elements */\nif let Some(pop_front) = deque.pop_front() { // Front element dequeue\n}\nif let Some(pop_rear) = deque.pop_back() {   // Rear element dequeue\n}\n\n/* Get deque length */\nlet size = deque.len();\n\n/* Check if deque is empty */\nlet is_empty = deque.is_empty();\n
    deque.c
    // C does not provide a built-in deque\n
    deque.kt
    /* Initialize deque */\nval deque = LinkedList<Int>()\n\n/* Enqueue elements */\ndeque.offerLast(2)  // Add to rear\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // Add to front\ndeque.offerFirst(1)\n\n/* Access elements */\nval peekFirst = deque.peekFirst() // Front element\nval peekLast = deque.peekLast()   // Rear element\n\n/* Dequeue elements */\nval popFirst = deque.pollFirst() // Front element dequeue\nval popLast = deque.pollLast()   // Rear element dequeue\n\n/* Get deque length */\nval size = deque.size\n\n/* Check if deque is empty */\nval isEmpty = deque.isEmpty()\n
    deque.rb
    # Initialize deque\n# Ruby does not have a built-in deque, can only use Array as a deque\ndeque = []\n\n# Enqueue elements\ndeque << 2\ndeque << 5\ndeque << 4\n# Please note that since it's an array, Array#unshift has O(n) time complexity\ndeque.unshift(3)\ndeque.unshift(1)\n\n# Access elements\npeek_first = deque.first\npeek_last = deque.last\n\n# Dequeue elements\n# Please note that since it's an array, Array#shift has O(n) time complexity\npop_front = deque.shift\npop_back = deque.pop\n\n# Get deque length\nsize = deque.length\n\n# Check if deque is empty\nis_empty = size.zero?\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532-deque-implementation","level":2,"title":"5.3.2   Deque Implementation *","text":"

    The implementation of a deque is similar to that of a queue. You can choose either a linked list or an array as the underlying data structure.

    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1-doubly-linked-list-implementation","level":3,"title":"1.   Doubly Linked List Implementation","text":"

    Reviewing the previous section, we used a regular singly linked list to implement a queue because it conveniently allows deleting the head node (corresponding to dequeue) and adding new nodes after the tail node (corresponding to enqueue).

    For a deque, both the front and rear can perform enqueue and dequeue operations. In other words, a deque needs to implement operations in the opposite direction as well. For this reason, we use a \"doubly linked list\" as the underlying data structure for the deque.

    As shown in Figure 5-8, we treat the head and tail nodes of the doubly linked list as the front and rear of the deque, implementing functionality to add and remove nodes at both ends.

    <1><2><3><4><5>

    Figure 5-8   Enqueue and dequeue operations in linked list implementation of deque

    The implementation code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py
    class ListNode:\n    \"\"\"Doubly linked list node\"\"\"\n\n    def __init__(self, val: int):\n        \"\"\"Constructor\"\"\"\n        self.val: int = val\n        self.next: ListNode | None = None  # Successor node reference\n        self.prev: ListNode | None = None  # Predecessor node reference\n\nclass LinkedListDeque:\n    \"\"\"Double-ended queue based on doubly linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._front: ListNode | None = None  # Head node front\n        self._rear: ListNode | None = None  # Tail node rear\n        self._size: int = 0  # Length of the double-ended queue\n\n    def size(self) -> int:\n        \"\"\"Get the length of the double-ended queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the double-ended queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int, is_front: bool):\n        \"\"\"Enqueue operation\"\"\"\n        node = ListNode(num)\n        # If the linked list is empty, make both front and rear point to node\n        if self.is_empty():\n            self._front = self._rear = node\n        # Front of the queue enqueue operation\n        elif is_front:\n            # Add node to the head of the linked list\n            self._front.prev = node\n            node.next = self._front\n            self._front = node  # Update head node\n        # Rear of the queue enqueue operation\n        else:\n            # Add node to the tail of the linked list\n            self._rear.next = node\n            node.prev = self._rear\n            self._rear = node  # Update tail node\n        self._size += 1  # Update queue length\n\n    def push_first(self, num: int):\n        \"\"\"Front of the queue enqueue\"\"\"\n        self.push(num, True)\n\n    def push_last(self, num: int):\n        \"\"\"Rear of the queue enqueue\"\"\"\n        self.push(num, False)\n\n    def pop(self, is_front: bool) -> int:\n        \"\"\"Dequeue operation\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        # Front of the queue dequeue operation\n        if is_front:\n            val: int = self._front.val  # Temporarily store head node value\n            # Delete head node\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  # Update head node\n        # Rear of the queue dequeue operation\n        else:\n            val: int = self._rear.val  # Temporarily store tail node value\n            # Delete tail node\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  # Update tail node\n        self._size -= 1  # Update queue length\n        return val\n\n    def pop_first(self) -> int:\n        \"\"\"Front of the queue dequeue\"\"\"\n        return self.pop(True)\n\n    def pop_last(self) -> int:\n        \"\"\"Rear of the queue dequeue\"\"\"\n        return self.pop(False)\n\n    def peek_first(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._front.val\n\n    def peek_last(self) -> int:\n        \"\"\"Access rear of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._rear.val\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return array for printing\"\"\"\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
    /* Doubly linked list node */\nstruct DoublyListNode {\n    int val;              // Node value\n    DoublyListNode *next; // Successor node pointer\n    DoublyListNode *prev; // Predecessor node pointer\n    DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n    }\n};\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n  private:\n    DoublyListNode *front, *rear; // Head node front, tail node rear\n    int queSize = 0;              // Length of the double-ended queue\n\n  public:\n    /* Constructor */\n    LinkedListDeque() : front(nullptr), rear(nullptr) {\n    }\n\n    /* Destructor */\n    ~LinkedListDeque() {\n        // Traverse linked list to delete nodes and free memory\n        DoublyListNode *pre, *cur = front;\n        while (cur != nullptr) {\n            pre = cur;\n            cur = cur->next;\n            delete pre;\n        }\n    }\n\n    /* Get the length of the double-ended queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue operation */\n    void push(int num, bool isFront) {\n        DoublyListNode *node = new DoublyListNode(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty())\n            front = rear = node;\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front->prev = node;\n            node->next = front;\n            front = node; // Update head node\n        // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear->next = node;\n            node->prev = rear;\n            rear = node; // Update tail node\n        }\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Dequeue operation */\n    int pop(bool isFront) {\n        if (isEmpty())\n            throw out_of_range(\"Queue is empty\");\n        int val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front->val; // Delete head node\n            // Delete head node\n            DoublyListNode *fNext = front->next;\n            if (fNext != nullptr) {\n                fNext->prev = nullptr;\n                front->next = nullptr;\n            }\n            delete front;\n            front = fNext; // Update head node\n        // Temporarily store tail node value\n        } else {\n            val = rear->val; // Delete tail node\n            // Update tail node\n            DoublyListNode *rPrev = rear->prev;\n            if (rPrev != nullptr) {\n                rPrev->next = nullptr;\n                rear->prev = nullptr;\n            }\n            delete rear;\n            rear = rPrev; // Update tail node\n        }\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    int popFirst() {\n        return pop(true);\n    }\n\n    /* Access rear of the queue element */\n    int popLast() {\n        return pop(false);\n    }\n\n    /* Return list for printing */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return front->val;\n    }\n\n    /* Driver Code */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return rear->val;\n    }\n\n    /* Return array for printing */\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
    /* Doubly linked list node */\nclass ListNode {\n    int val; // Node value\n    ListNode next; // Successor node reference\n    ListNode prev; // Predecessor node reference\n\n    ListNode(int val) {\n        this.val = val;\n        prev = next = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private ListNode front, rear; // Head node front, tail node rear\n    private int queSize = 0; // Length of the double-ended queue\n\n    public LinkedListDeque() {\n        front = rear = null;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue operation */\n    private void push(int num, boolean isFront) {\n        ListNode node = new ListNode(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty())\n            front = rear = node;\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front.prev = node;\n            node.next = front;\n            front = node; // Update head node\n        // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear.next = node;\n            node.prev = rear;\n            rear = node; // Update tail node\n        }\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    public void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    public void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Dequeue operation */\n    private int pop(boolean isFront) {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        int val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front.val; // Delete head node\n            // Delete head node\n            ListNode fNext = front.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front.next = null;\n            }\n            front = fNext; // Update head node\n        // Temporarily store tail node value\n        } else {\n            val = rear.val; // Delete tail node\n            // Update tail node\n            ListNode rPrev = rear.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear.prev = null;\n            }\n            rear = rPrev; // Update tail node\n        }\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    public int popFirst() {\n        return pop(true);\n    }\n\n    /* Access rear of the queue element */\n    public int popLast() {\n        return pop(false);\n    }\n\n    /* Return list for printing */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Driver Code */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return rear.val;\n    }\n\n    /* Return array for printing */\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
    /* Doubly linked list node */\nclass ListNode(int val) {\n    public int val = val;       // Node value\n    public ListNode? next = null; // Successor node reference\n    public ListNode? prev = null; // Predecessor node reference\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    ListNode? front, rear; // Head node front, tail node rear\n    int queSize = 0;      // Length of the double-ended queue\n\n    public LinkedListDeque() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Enqueue operation */\n    void Push(int num, bool isFront) {\n        ListNode node = new(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (IsEmpty()) {\n            front = node;\n            rear = node;\n        }\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front!.prev = node;\n            node.next = front;\n            front = node; // Update head node\n        }\n        // Rear of the queue enqueue operation\n        else {\n            // Add node to the tail of the linked list\n            rear!.next = node;\n            node.prev = rear;\n            rear = node;  // Update tail node\n        }\n\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    public void PushFirst(int num) {\n        Push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    public void PushLast(int num) {\n        Push(num, false);\n    }\n\n    /* Dequeue operation */\n    int? Pop(bool isFront) {\n        if (IsEmpty())\n            throw new Exception();\n        int? val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front?.val; // Delete head node\n            // Delete head node\n            ListNode? fNext = front?.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front!.next = null;\n            }\n            front = fNext;   // Update head node\n        }\n        // Temporarily store tail node value\n        else {\n            val = rear?.val;  // Delete tail node\n            // Update tail node\n            ListNode? rPrev = rear?.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear!.prev = null;\n            }\n            rear = rPrev;    // Update tail node\n        }\n\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    public int? PopFirst() {\n        return Pop(true);\n    }\n\n    /* Access rear of the queue element */\n    public int? PopLast() {\n        return Pop(false);\n    }\n\n    /* Return list for printing */\n    public int? PeekFirst() {\n        if (IsEmpty())\n            throw new Exception();\n        return front?.val;\n    }\n\n    /* Driver Code */\n    public int? PeekLast() {\n        if (IsEmpty())\n            throw new Exception();\n        return rear?.val;\n    }\n\n    /* Return array for printing */\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
    /* Double-ended queue based on doubly linked list implementation */\ntype linkedListDeque struct {\n    // Use built-in package list\n    data *list.List\n}\n\n/* Initialize deque */\nfunc newLinkedListDeque() *linkedListDeque {\n    return &linkedListDeque{\n        data: list.New(),\n    }\n}\n\n/* Front element enqueue */\nfunc (s *linkedListDeque) pushFirst(value any) {\n    s.data.PushFront(value)\n}\n\n/* Rear element enqueue */\nfunc (s *linkedListDeque) pushLast(value any) {\n    s.data.PushBack(value)\n}\n\n/* Check if the double-ended queue is empty */\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/* Rear element dequeue */\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/* Return list for printing */\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/* Driver Code */\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/* Get the length of the queue */\nfunc (s *linkedListDeque) size() int {\n    return s.data.Len()\n}\n\n/* Check if the queue is empty */\nfunc (s *linkedListDeque) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListDeque) toList() *list.List {\n    return s.data\n}\n
    linkedlist_deque.swift
    /* Doubly linked list node */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Successor node reference\n    weak var prev: ListNode? // Predecessor node reference\n\n    init(val: Int) {\n        self.val = val\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private var front: ListNode? // Head node front\n    private var rear: ListNode? // Tail node rear\n    private var _size: Int // Length of the double-ended queue\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the double-ended queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the double-ended queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue operation */\n    private func push(num: Int, isFront: Bool) {\n        let node = ListNode(val: num)\n        // If the linked list is empty, make both front and rear point to node\n        if isEmpty() {\n            front = node\n            rear = node\n        }\n        // Front of the queue enqueue operation\n        else if isFront {\n            // Add node to the head of the linked list\n            front?.prev = node\n            node.next = front\n            front = node // Update head node\n        }\n        // Rear of the queue enqueue operation\n        else {\n            // Add node to the tail of the linked list\n            rear?.next = node\n            node.prev = rear\n            rear = node // Update tail node\n        }\n        _size += 1 // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    func pushFirst(num: Int) {\n        push(num: num, isFront: true)\n    }\n\n    /* Rear of the queue enqueue */\n    func pushLast(num: Int) {\n        push(num: num, isFront: false)\n    }\n\n    /* Dequeue operation */\n    private func pop(isFront: Bool) -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        let val: Int\n        // Temporarily store head node value\n        if isFront {\n            val = front!.val // Delete head node\n            // Delete head node\n            let fNext = front?.next\n            if fNext != nil {\n                fNext?.prev = nil\n                front?.next = nil\n            }\n            front = fNext // Update head node\n        }\n        // Temporarily store tail node value\n        else {\n            val = rear!.val // Delete tail node\n            // Update tail node\n            let rPrev = rear?.prev\n            if rPrev != nil {\n                rPrev?.next = nil\n                rear?.prev = nil\n            }\n            rear = rPrev // Update tail node\n        }\n        _size -= 1 // Update queue length\n        return val\n    }\n\n    /* Rear of the queue dequeue */\n    func popFirst() -> Int {\n        pop(isFront: true)\n    }\n\n    /* Access rear of the queue element */\n    func popLast() -> Int {\n        pop(isFront: false)\n    }\n\n    /* Return list for printing */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return front!.val\n    }\n\n    /* Driver Code */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return rear!.val\n    }\n\n    /* Return array for printing */\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
    /* Doubly linked list node */\nclass ListNode {\n    prev; // Predecessor node reference (pointer)\n    next; // Successor node reference (pointer)\n    val; // Node value\n\n    constructor(val) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    #front; // Head node front\n    #rear; // Tail node rear\n    #queSize; // Length of the double-ended queue\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n        this.#queSize = 0;\n    }\n\n    /* Rear of the queue enqueue operation */\n    pushLast(val) {\n        const node = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Add node to the tail of the linked list\n            this.#rear.next = node;\n            node.prev = this.#rear;\n            this.#rear = node; // Update tail node\n        }\n        this.#queSize++;\n    }\n\n    /* Front of the queue enqueue operation */\n    pushFirst(val) {\n        const node = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Add node to the head of the linked list\n            this.#front.prev = node;\n            node.next = this.#front;\n            this.#front = node; // Update head node\n        }\n        this.#queSize++;\n    }\n\n    /* Temporarily store tail node value */\n    popLast() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#rear.val; // Store tail node value\n        // Update tail node\n        let temp = this.#rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.#rear.prev = null;\n        }\n        this.#rear = temp; // Update tail node\n        this.#queSize--;\n        return value;\n    }\n\n    /* Temporarily store head node value */\n    popFirst() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#front.val; // Store tail node value\n        // Delete head node\n        let temp = this.#front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.#front.next = null;\n        }\n        this.#front = temp; // Update head node\n        this.#queSize--;\n        return value;\n    }\n\n    /* Driver Code */\n    peekLast() {\n        return this.#queSize === 0 ? null : this.#rear.val;\n    }\n\n    /* Return list for printing */\n    peekFirst() {\n        return this.#queSize === 0 ? null : this.#front.val;\n    }\n\n    /* Get the length of the double-ended queue */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Print deque */\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
    /* Doubly linked list node */\nclass ListNode {\n    prev: ListNode; // Predecessor node reference (pointer)\n    next: ListNode; // Successor node reference (pointer)\n    val: number; // Node value\n\n    constructor(val: number) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private front: ListNode; // Head node front\n    private rear: ListNode; // Tail node rear\n    private queSize: number; // Length of the double-ended queue\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n        this.queSize = 0;\n    }\n\n    /* Rear of the queue enqueue operation */\n    pushLast(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Add node to the tail of the linked list\n            this.rear.next = node;\n            node.prev = this.rear;\n            this.rear = node; // Update tail node\n        }\n        this.queSize++;\n    }\n\n    /* Front of the queue enqueue operation */\n    pushFirst(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Add node to the head of the linked list\n            this.front.prev = node;\n            node.next = this.front;\n            this.front = node; // Update head node\n        }\n        this.queSize++;\n    }\n\n    /* Temporarily store tail node value */\n    popLast(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.rear.val; // Store tail node value\n        // Update tail node\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; // Update tail node\n        this.queSize--;\n        return value;\n    }\n\n    /* Temporarily store head node value */\n    popFirst(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.front.val; // Store tail node value\n        // Delete head node\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; // Update head node\n        this.queSize--;\n        return value;\n    }\n\n    /* Driver Code */\n    peekLast(): number {\n        return this.queSize === 0 ? null : this.rear.val;\n    }\n\n    /* Return list for printing */\n    peekFirst(): number {\n        return this.queSize === 0 ? null : this.front.val;\n    }\n\n    /* Get the length of the double-ended queue */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Print deque */\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
    /* Doubly linked list node */\nclass ListNode {\n  int val; // Node value\n  ListNode? next; // Successor node reference\n  ListNode? prev; // Predecessor node reference\n\n  ListNode(this.val, {this.next, this.prev});\n}\n\n/* Deque implemented based on doubly linked list */\nclass LinkedListDeque {\n  late ListNode? _front; // Head node _front\n  late ListNode? _rear; // Tail node _rear\n  int _queSize = 0; // Length of the double-ended queue\n\n  LinkedListDeque() {\n    this._front = null;\n    this._rear = null;\n  }\n\n  /* Get deque length */\n  int size() {\n    return this._queSize;\n  }\n\n  /* Check if the double-ended queue is empty */\n  bool isEmpty() {\n    return size() == 0;\n  }\n\n  /* Enqueue operation */\n  void push(int _num, bool isFront) {\n    final ListNode node = ListNode(_num);\n    if (isEmpty()) {\n      // If list is empty, let both _front and _rear point to node\n      _front = _rear = node;\n    } else if (isFront) {\n      // Front of the queue enqueue operation\n      // Add node to the head of the linked list\n      _front!.prev = node;\n      node.next = _front;\n      _front = node; // Update head node\n    } else {\n      // Rear of the queue enqueue operation\n      // Add node to the tail of the linked list\n      _rear!.next = node;\n      node.prev = _rear;\n      _rear = node; // Update tail node\n    }\n    _queSize++; // Update queue length\n  }\n\n  /* Front of the queue enqueue */\n  void pushFirst(int _num) {\n    push(_num, true);\n  }\n\n  /* Rear of the queue enqueue */\n  void pushLast(int _num) {\n    push(_num, false);\n  }\n\n  /* Dequeue operation */\n  int? pop(bool isFront) {\n    // If queue is empty, return null directly\n    if (isEmpty()) {\n      return null;\n    }\n    final int val;\n    if (isFront) {\n      // Temporarily store head node value\n      val = _front!.val; // Delete head node\n      // Delete head node\n      ListNode? fNext = _front!.next;\n      if (fNext != null) {\n        fNext.prev = null;\n        _front!.next = null;\n      }\n      _front = fNext; // Update head node\n    } else {\n      // Temporarily store tail node value\n      val = _rear!.val; // Delete tail node\n      // Update tail node\n      ListNode? rPrev = _rear!.prev;\n      if (rPrev != null) {\n        rPrev.next = null;\n        _rear!.prev = null;\n      }\n      _rear = rPrev; // Update tail node\n    }\n    _queSize--; // Update queue length\n    return val;\n  }\n\n  /* Rear of the queue dequeue */\n  int? popFirst() {\n    return pop(true);\n  }\n\n  /* Access rear of the queue element */\n  int? popLast() {\n    return pop(false);\n  }\n\n  /* Return list for printing */\n  int? peekFirst() {\n    return _front?.val;\n  }\n\n  /* Driver Code */\n  int? peekLast() {\n    return _rear?.val;\n  }\n\n  /* Return array for printing */\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
    /* Doubly linked list node */\npub struct ListNode<T> {\n    pub val: T,                                 // Node value\n    pub next: Option<Rc<RefCell<ListNode<T>>>>, // Successor node pointer\n    pub prev: Option<Rc<RefCell<ListNode<T>>>>, // Predecessor node pointer\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/* Double-ended queue based on doubly linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListDeque<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Head node front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Tail node rear\n    que_size: usize,                         // Length of the double-ended queue\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    /* Get the length of the double-ended queue */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Check if the double-ended queue is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Enqueue operation */\n    fn push(&mut self, num: T, is_front: bool) {\n        let node = ListNode::new(num);\n        // Front of the queue enqueue operation\n        if is_front {\n            match self.front.take() {\n                // If the linked list is empty, make both front and rear point to node\n                None => {\n                    self.rear = Some(node.clone());\n                    self.front = Some(node);\n                }\n                // Add node to the head of the linked list\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); // Update head node\n                }\n            }\n        }\n        // Rear of the queue enqueue operation\n        else {\n            match self.rear.take() {\n                // If the linked list is empty, make both front and rear point to node\n                None => {\n                    self.front = Some(node.clone());\n                    self.rear = Some(node);\n                }\n                // Add node to the tail of the linked list\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); // Update tail node\n                }\n            }\n        }\n        self.que_size += 1; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    pub fn push_first(&mut self, num: T) {\n        self.push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    pub fn push_last(&mut self, num: T) {\n        self.push(num, false);\n    }\n\n    /* Dequeue operation */\n    fn pop(&mut self, is_front: bool) -> Option<T> {\n        // If queue is empty, return None directly\n        if self.is_empty() {\n            return None;\n        };\n        // Temporarily store head node value\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); // Update head node\n                    }\n                    None => {\n                        self.rear.take();\n                    }\n                }\n                self.que_size -= 1; // Update queue length\n                old_front.borrow().val\n            })\n        }\n        // Temporarily store tail node value\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); // Update tail node\n                    }\n                    None => {\n                        self.front.take();\n                    }\n                }\n                self.que_size -= 1; // Update queue length\n                old_rear.borrow().val\n            })\n        }\n    }\n\n    /* Rear of the queue dequeue */\n    pub fn pop_first(&mut self) -> Option<T> {\n        return self.pop(true);\n    }\n\n    /* Access rear of the queue element */\n    pub fn pop_last(&mut self) -> Option<T> {\n        return self.pop(false);\n    }\n\n    /* Return list for printing */\n    pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Driver Code */\n    pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.rear.as_ref()\n    }\n\n    /* Return array for printing */\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
    /* Doubly linked list node */\ntypedef struct DoublyListNode {\n    int val;                     // Node value\n    struct DoublyListNode *next; // Successor node\n    struct DoublyListNode *prev; // Predecessor node\n} DoublyListNode;\n\n/* Constructor */\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/* Destructor */\nvoid delDoublyListNode(DoublyListNode *node) {\n    free(node);\n}\n\n/* Double-ended queue based on doubly linked list implementation */\ntypedef struct {\n    DoublyListNode *front, *rear; // Head node front, tail node rear\n    int queSize;                  // Length of the double-ended queue\n} LinkedListDeque;\n\n/* Constructor */\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/* Destructor */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n    // Free all nodes\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    // Free deque structure\n    free(deque);\n}\n\n/* Get the length of the queue */\nint size(LinkedListDeque *deque) {\n    return deque->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(LinkedListDeque *deque) {\n    return (size(deque) == 0);\n}\n\n/* Enqueue */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n    DoublyListNode *node = newDoublyListNode(num);\n    // If list is empty, set both front and rear to node\n    if (empty(deque)) {\n        deque->front = deque->rear = node;\n    }\n    // Front of the queue enqueue operation\n    else if (isFront) {\n        // Add node to the head of the linked list\n        deque->front->prev = node;\n        node->next = deque->front;\n        deque->front = node; // Update head node\n    }\n    // Rear of the queue enqueue operation\n    else {\n        // Add node to the tail of the linked list\n        deque->rear->next = node;\n        node->prev = deque->rear;\n        deque->rear = node;\n    }\n    deque->queSize++; // Update queue length\n}\n\n/* Front of the queue enqueue */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n    push(deque, num, true);\n}\n\n/* Rear of the queue enqueue */\nvoid pushLast(LinkedListDeque *deque, int num) {\n    push(deque, num, false);\n}\n\n/* Return list for printing */\nint peekFirst(LinkedListDeque *deque) {\n    assert(size(deque) && deque->front);\n    return deque->front->val;\n}\n\n/* Driver Code */\nint peekLast(LinkedListDeque *deque) {\n    assert(size(deque) && deque->rear);\n    return deque->rear->val;\n}\n\n/* Dequeue */\nint pop(LinkedListDeque *deque, bool isFront) {\n    if (empty(deque))\n        return -1;\n    int val;\n    // Temporarily store head node value\n    if (isFront) {\n        val = peekFirst(deque); // Delete head node\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; // Update head node\n    }\n    // Temporarily store tail node value\n    else {\n        val = peekLast(deque); // Delete tail node\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; // Update tail node\n    }\n    deque->queSize--; // Update queue length\n    return val;\n}\n\n/* Rear of the queue dequeue */\nint popFirst(LinkedListDeque *deque) {\n    return pop(deque, true);\n}\n\n/* Access rear of the queue element */\nint popLast(LinkedListDeque *deque) {\n    return pop(deque, false);\n}\n\n/* Print queue */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n    int *arr = malloc(sizeof(int) * deque->queSize);\n    // Copy data from list to array\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
    /* Doubly linked list node */\nclass ListNode(var _val: Int) {\n    // Node value\n    var next: ListNode? = null // Successor node reference\n    var prev: ListNode? = null // Predecessor node reference\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private var front: ListNode? = null // Head node front\n    private var rear: ListNode? = null // Tail node rear\n    private var queSize: Int = 0 // Length of the double-ended queue\n\n    /* Get the length of the double-ended queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the double-ended queue is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Enqueue operation */\n    fun push(num: Int, isFront: Boolean) {\n        val node = ListNode(num)\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty()) {\n            rear = node\n            front = rear\n            // Front of the queue enqueue operation\n        } else if (isFront) {\n            // Add node to the head of the linked list\n            front?.prev = node\n            node.next = front\n            front = node // Update head node\n            // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear?.next = node\n            node.prev = rear\n            rear = node // Update tail node\n        }\n        queSize++ // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    fun pushFirst(num: Int) {\n        push(num, true)\n    }\n\n    /* Rear of the queue enqueue */\n    fun pushLast(num: Int) {\n        push(num, false)\n    }\n\n    /* Dequeue operation */\n    fun pop(isFront: Boolean): Int {\n        if (isEmpty()) \n            throw IndexOutOfBoundsException()\n        val _val: Int\n        // Temporarily store head node value\n        if (isFront) {\n            _val = front!!._val // Delete head node\n            // Delete head node\n            val fNext = front!!.next\n            if (fNext != null) {\n                fNext.prev = null\n                front!!.next = null\n            }\n            front = fNext // Update head node\n            // Temporarily store tail node value\n        } else {\n            _val = rear!!._val // Delete tail node\n            // Update tail node\n            val rPrev = rear!!.prev\n            if (rPrev != null) {\n                rPrev.next = null\n                rear!!.prev = null\n            }\n            rear = rPrev // Update tail node\n        }\n        queSize-- // Update queue length\n        return _val\n    }\n\n    /* Rear of the queue dequeue */\n    fun popFirst(): Int {\n        return pop(true)\n    }\n\n    /* Access rear of the queue element */\n    fun popLast(): Int {\n        return pop(false)\n    }\n\n    /* Return list for printing */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Driver Code */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return rear!!._val\n    }\n\n    /* Return array for printing */\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### Doubly linked list node\nclass ListNode\n  attr_accessor :val\n  attr_accessor :next # Successor node reference\n  attr_accessor :prev # Predecessor node reference\n\n  ### Constructor ###\n  def initialize(val)\n    @val = val\n  end\nend\n\n### Deque based on doubly linked list ###\nclass LinkedListDeque\n  ### Get deque length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @front = nil  # Head node front\n    @rear = nil   # Tail node rear\n    @size = 0     # Length of the double-ended queue\n  end\n\n  ### Check if deque is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue operation ###\n  def push(num, is_front)\n    node = ListNode.new(num)\n    # If list is empty, set both front and rear to node\n    if is_empty?\n      @front = @rear = node\n    # Front of the queue enqueue operation\n    elsif is_front\n      # Add node to the head of the linked list\n      @front.prev = node\n      node.next = @front\n      @front = node # Update head node\n    # Rear of the queue enqueue operation\n    else\n      # Add node to the tail of the linked list\n      @rear.next = node\n      node.prev = @rear\n      @rear = node # Update tail node\n    end\n    @size += 1 # Update queue length\n  end\n\n  ### Enqueue at front ###\n  def push_first(num)\n    push(num, true)\n  end\n\n  ### Enqueue at rear ###\n  def push_last(num)\n    push(num, false)\n  end\n\n  ### Dequeue operation ###\n  def pop(is_front)\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    # Temporarily store head node value\n    if is_front\n      val = @front.val # Delete head node\n      # Delete head node\n      fnext = @front.next\n      unless fnext.nil?\n        fnext.prev = nil\n        @front.next = nil\n      end\n      @front = fnext # Update head node\n    # Temporarily store tail node value\n    else\n      val = @rear.val # Delete tail node\n      # Update tail node\n      rprev = @rear.prev\n      unless rprev.nil?\n        rprev.next = nil\n        @rear.prev = nil\n      end\n      @rear = rprev # Update tail node\n    end\n    @size -= 1 # Update queue length\n\n    val\n  end\n\n  ### Dequeue from front ###\n  def pop_first\n    pop(true)\n  end\n\n  ### Dequeue from front ###\n  def pop_last\n    pop(false)\n  end\n\n  ### Access front element ###\n  def peek_first\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @front.val\n  end\n\n  ### Access rear element ###\n  def peek_last\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @rear.val\n  end\n\n  ### Return array for printing ###\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":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

    As shown in Figure 5-9, similar to implementing a queue based on an array, we can also use a circular array to implement a deque.

    <1><2><3><4><5>

    Figure 5-9   Enqueue and dequeue operations in array implementation of deque

    Based on the queue implementation, we only need to add methods for \"enqueue at front\" and \"dequeue from rear\":

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py
    class ArrayDeque:\n    \"\"\"Double-ended queue based on circular array implementation\"\"\"\n\n    def __init__(self, capacity: int):\n        \"\"\"Constructor\"\"\"\n        self._nums: list[int] = [0] * capacity\n        self._front: int = 0\n        self._size: int = 0\n\n    def capacity(self) -> int:\n        \"\"\"Get the capacity of the double-ended queue\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Get the length of the double-ended queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the double-ended queue is empty\"\"\"\n        return self._size == 0\n\n    def index(self, i: int) -> int:\n        \"\"\"Calculate circular array index\"\"\"\n        # Use modulo operation to wrap the array head and tail together\n        # When i passes the tail of the array, return to the head\n        # When i passes the head of the array, return to the tail\n        return (i + self.capacity()) % self.capacity()\n\n    def push_first(self, num: int):\n        \"\"\"Front of the queue enqueue\"\"\"\n        if self._size == self.capacity():\n            print(\"Double-ended queue is full\")\n            return\n        # Front pointer moves one position to the left\n        # Use modulo operation to wrap front around to the tail after passing the head of the array\n        self._front = self.index(self._front - 1)\n        # Add num to the front of the queue\n        self._nums[self._front] = num\n        self._size += 1\n\n    def push_last(self, num: int):\n        \"\"\"Rear of the queue enqueue\"\"\"\n        if self._size == self.capacity():\n            print(\"Double-ended queue is full\")\n            return\n        # Calculate rear pointer, points to rear index + 1\n        rear = self.index(self._front + self._size)\n        # Add num to the rear of the queue\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop_first(self) -> int:\n        \"\"\"Front of the queue dequeue\"\"\"\n        num = self.peek_first()\n        # Front pointer moves one position backward\n        self._front = self.index(self._front + 1)\n        self._size -= 1\n        return num\n\n    def pop_last(self) -> int:\n        \"\"\"Rear of the queue dequeue\"\"\"\n        num = self.peek_last()\n        self._size -= 1\n        return num\n\n    def peek_first(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._nums[self._front]\n\n    def peek_last(self) -> int:\n        \"\"\"Access rear of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        # Calculate tail element index\n        last = self.index(self._front + self._size - 1)\n        return self._nums[last]\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return array for printing\"\"\"\n        # Only convert list elements within the valid length range\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n  private:\n    vector<int> nums; // Array for storing double-ended queue elements\n    int front;        // Front pointer, points to the front of the queue element\n    int queSize;      // Double-ended queue length\n\n  public:\n    /* Constructor */\n    ArrayDeque(int capacity) {\n        nums.resize(capacity);\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    int capacity() {\n        return nums.size();\n    }\n\n    /* Get the length of the double-ended queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    int index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity();\n    }\n\n    /* Front of the queue enqueue */\n    void pushFirst(int num) {\n        if (queSize == capacity()) {\n            cout << \"Double-ended queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    void pushLast(int num) {\n        if (queSize == capacity()) {\n            cout << \"Double-ended queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    int popFirst() {\n        int num = peekFirst();\n        // Move front pointer backward by one position\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return nums[front];\n    }\n\n    /* Driver Code */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        // Initialize double-ended queue\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    vector<int> toVector() {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private int[] nums; // Array for storing double-ended queue elements\n    private int front; // Front pointer, points to the front of the queue element\n    private int queSize; // Double-ended queue length\n\n    /* Constructor */\n    public ArrayDeque(int capacity) {\n        this.nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    private int index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity();\n    }\n\n    /* Front of the queue enqueue */\n    public void pushFirst(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    public void pushLast(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    public int popFirst() {\n        int num = peekFirst();\n        // Move front pointer backward by one position\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    public int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Driver Code */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        // Initialize double-ended queue\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    public int[] toArray() {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    int[] nums;  // Array for storing double-ended queue elements\n    int front;   // Front pointer, points to the front of the queue element\n    int queSize; // Double-ended queue length\n\n    /* Constructor */\n    public ArrayDeque(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    int Index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + Capacity()) % Capacity();\n    }\n\n    /* Front of the queue enqueue */\n    public void PushFirst(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = Index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    public void PushLast(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = Index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    public int PopFirst() {\n        int num = PeekFirst();\n        // Move front pointer backward by one position\n        front = Index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    public int PopLast() {\n        int num = PeekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int PeekFirst() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        return nums[front];\n    }\n\n    /* Driver Code */\n    public int PeekLast() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        // Initialize double-ended queue\n        int last = Index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    public int[] ToArray() {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\ntype arrayDeque struct {\n    nums        []int // Array for storing double-ended queue elements\n    front       int   // Front pointer, points to the front of the queue element\n    queSize     int   // Double-ended queue length\n    queCapacity int   // Queue capacity (maximum number of elements)\n}\n\n/* Access front of the queue element */\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/* Get the length of the double-ended queue */\nfunc (q *arrayDeque) size() int {\n    return q.queSize\n}\n\n/* Check if the double-ended queue is empty */\nfunc (q *arrayDeque) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Calculate circular array index */\nfunc (q *arrayDeque) index(i int) int {\n    // Use modulo operation to wrap the array head and tail together\n    // When i passes the tail of the array, return to the head\n    // When i passes the head of the array, return to the tail\n    return (i + q.queCapacity) % q.queCapacity\n}\n\n/* Front of the queue enqueue */\nfunc (q *arrayDeque) pushFirst(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Double-ended queue is full\")\n        return\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Add num to the front of the queue\n    q.front = q.index(q.front - 1)\n    // Add num to front of queue\n    q.nums[q.front] = num\n    q.queSize++\n}\n\n/* Rear of the queue enqueue */\nfunc (q *arrayDeque) pushLast(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Double-ended queue is full\")\n        return\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    rear := q.index(q.front + q.queSize)\n    // Front pointer moves one position backward\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Rear of the queue dequeue */\nfunc (q *arrayDeque) popFirst() any {\n    num := q.peekFirst()\n    if num == nil {\n        return nil\n    }\n    // Move front pointer backward by one position\n    q.front = q.index(q.front + 1)\n    q.queSize--\n    return num\n}\n\n/* Access rear of the queue element */\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/* Return list for printing */\nfunc (q *arrayDeque) peekFirst() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Driver Code */\nfunc (q *arrayDeque) peekLast() any {\n    if q.isEmpty() {\n        return nil\n    }\n    // Initialize double-ended queue\n    last := q.index(q.front + q.queSize - 1)\n    return q.nums[last]\n}\n\n/* Get Slice for printing */\nfunc (q *arrayDeque) toSlice() []int {\n    // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private var nums: [Int] // Array for storing double-ended queue elements\n    private var front: Int // Front pointer, points to the front of the queue element\n    private var _size: Int // Double-ended queue length\n\n    /* Constructor */\n    init(capacity: Int) {\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Get the capacity of the double-ended queue */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Get the length of the double-ended queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the double-ended queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Calculate circular array index */\n    private func index(i: Int) -> Int {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        (i + capacity()) % capacity()\n    }\n\n    /* Front of the queue enqueue */\n    func pushFirst(num: Int) {\n        if size() == capacity() {\n            print(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(i: front - 1)\n        // Add num to front of queue\n        nums[front] = num\n        _size += 1\n    }\n\n    /* Rear of the queue enqueue */\n    func pushLast(num: Int) {\n        if size() == capacity() {\n            print(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        let rear = index(i: front + size())\n        // Front pointer moves one position backward\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Rear of the queue dequeue */\n    func popFirst() -> Int {\n        let num = peekFirst()\n        // Move front pointer backward by one position\n        front = index(i: front + 1)\n        _size -= 1\n        return num\n    }\n\n    /* Access rear of the queue element */\n    func popLast() -> Int {\n        let num = peekLast()\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return nums[front]\n    }\n\n    /* Driver Code */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        // Initialize double-ended queue\n        let last = index(i: front + size() - 1)\n        return nums[last]\n    }\n\n    /* Return array for printing */\n    func toArray() -> [Int] {\n        // Elements enqueue\n        (front ..< front + size()).map { nums[index(i: $0)] }\n    }\n}\n
    array_deque.js
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    #nums; // Array for storing double-ended queue elements\n    #front; // Front pointer, points to the front of the queue element\n    #queSize; // Double-ended queue length\n\n    /* Constructor */\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n        this.#front = 0;\n        this.#queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    capacity() {\n        return this.#nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Calculate circular array index */\n    index(i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Front of the queue enqueue */\n    pushFirst(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        this.#front = this.index(this.#front - 1);\n        // Add num to front of queue\n        this.#nums[this.#front] = num;\n        this.#queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    pushLast(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        const rear = this.index(this.#front + this.#queSize);\n        // Front pointer moves one position backward\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    popFirst() {\n        const num = this.peekFirst();\n        // Move front pointer backward by one position\n        this.#front = this.index(this.#front + 1);\n        this.#queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    popLast() {\n        const num = this.peekLast();\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peekFirst() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.#nums[this.#front];\n    }\n\n    /* Driver Code */\n    peekLast() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Initialize double-ended queue\n        const last = this.index(this.#front + this.#queSize - 1);\n        return this.#nums[last];\n    }\n\n    /* Return array for printing */\n    toArray() {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private nums: number[]; // Array for storing double-ended queue elements\n    private front: number; // Front pointer, points to the front of the queue element\n    private queSize: number; // Double-ended queue length\n\n    /* Constructor */\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = 0;\n        this.queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Calculate circular array index */\n    index(i: number): number {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Front of the queue enqueue */\n    pushFirst(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        this.front = this.index(this.front - 1);\n        // Add num to front of queue\n        this.nums[this.front] = num;\n        this.queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    pushLast(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        const rear: number = this.index(this.front + this.queSize);\n        // Front pointer moves one position backward\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    popFirst(): number {\n        const num: number = this.peekFirst();\n        // Move front pointer backward by one position\n        this.front = this.index(this.front + 1);\n        this.queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    popLast(): number {\n        const num: number = this.peekLast();\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peekFirst(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.nums[this.front];\n    }\n\n    /* Driver Code */\n    peekLast(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Initialize double-ended queue\n        const last = this.index(this.front + this.queSize - 1);\n        return this.nums[last];\n    }\n\n    /* Return array for printing */\n    toArray(): number[] {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n  late List<int> _nums; // Array for storing double-ended queue elements\n  late int _front; // Front pointer, points to the front of the queue element\n  late int _queSize; // Double-ended queue length\n\n  /* Constructor */\n  ArrayDeque(int capacity) {\n    this._nums = List.filled(capacity, 0);\n    this._front = this._queSize = 0;\n  }\n\n  /* Get the capacity of the double-ended queue */\n  int capacity() {\n    return _nums.length;\n  }\n\n  /* Get the length of the double-ended queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the double-ended queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Calculate circular array index */\n  int index(int i) {\n    // Use modulo operation to wrap the array head and tail together\n    // When i passes the tail of the array, return to the head\n    // When i passes the head of the array, return to the tail\n    return (i + capacity()) % capacity();\n  }\n\n  /* Front of the queue enqueue */\n  void pushFirst(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Double-ended queue is full\");\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Use modulo operation to wrap _front from array head back to tail\n    _front = index(_front - 1);\n    // Add _num to queue front\n    _nums[_front] = _num;\n    _queSize++;\n  }\n\n  /* Rear of the queue enqueue */\n  void pushLast(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Double-ended queue is full\");\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    int rear = index(_front + _queSize);\n    // Add _num to queue rear\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Rear of the queue dequeue */\n  int popFirst() {\n    int _num = peekFirst();\n    // Move front pointer right by one\n    _front = index(_front + 1);\n    _queSize--;\n    return _num;\n  }\n\n  /* Access rear of the queue element */\n  int popLast() {\n    int _num = peekLast();\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peekFirst() {\n    if (isEmpty()) {\n      throw Exception(\"Deque is empty\");\n    }\n    return _nums[_front];\n  }\n\n  /* Driver Code */\n  int peekLast() {\n    if (isEmpty()) {\n      throw Exception(\"Deque is empty\");\n    }\n    // Initialize double-ended queue\n    int last = index(_front + _queSize - 1);\n    return _nums[last];\n  }\n\n  /* Return array for printing */\n  List<int> toArray() {\n    // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nstruct ArrayDeque<T> {\n    nums: Vec<T>,    // Array for storing double-ended queue elements\n    front: usize,    // Front pointer, points to the front of the queue element\n    que_size: usize, // Double-ended queue length\n}\n\nimpl<T: Copy + Default> ArrayDeque<T> {\n    /* Constructor */\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    /* Get the capacity of the double-ended queue */\n    pub fn capacity(&self) -> usize {\n        self.nums.len()\n    }\n\n    /* Get the length of the double-ended queue */\n    pub fn size(&self) -> usize {\n        self.que_size\n    }\n\n    /* Check if the double-ended queue is empty */\n    pub fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Calculate circular array index */\n    fn index(&self, i: i32) -> usize {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n    }\n\n    /* Front of the queue enqueue */\n    pub fn push_first(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        self.front = self.index(self.front as i32 - 1);\n        // Add num to front of queue\n        self.nums[self.front] = num;\n        self.que_size += 1;\n    }\n\n    /* Rear of the queue enqueue */\n    pub fn push_last(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        let rear = self.index(self.front as i32 + self.que_size as i32);\n        // Front pointer moves one position backward\n        self.nums[rear] = num;\n        self.que_size += 1;\n    }\n\n    /* Rear of the queue dequeue */\n    fn pop_first(&mut self) -> T {\n        let num = self.peek_first();\n        // Move front pointer backward by one position\n        self.front = self.index(self.front as i32 + 1);\n        self.que_size -= 1;\n        num\n    }\n\n    /* Access rear of the queue element */\n    fn pop_last(&mut self) -> T {\n        let num = self.peek_last();\n        self.que_size -= 1;\n        num\n    }\n\n    /* Return list for printing */\n    fn peek_first(&self) -> T {\n        if self.is_empty() {\n            panic!(\"Deque is empty\")\n        };\n        self.nums[self.front]\n    }\n\n    /* Driver Code */\n    fn peek_last(&self) -> T {\n        if self.is_empty() {\n            panic!(\"Deque is empty\")\n        };\n        // Initialize double-ended queue\n        let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n        self.nums[last]\n    }\n\n    /* Return array for printing */\n    fn to_array(&self) -> Vec<T> {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\ntypedef struct {\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Rear pointer, points to rear + 1\n    int queCapacity; // Queue capacity\n} ArrayDeque;\n\n/* Constructor */\nArrayDeque *newArrayDeque(int capacity) {\n    ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n    // Initialize array\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/* Destructor */\nvoid delArrayDeque(ArrayDeque *deque) {\n    free(deque->nums);\n    free(deque);\n}\n\n/* Get the capacity of the double-ended queue */\nint capacity(ArrayDeque *deque) {\n    return deque->queCapacity;\n}\n\n/* Get the length of the double-ended queue */\nint size(ArrayDeque *deque) {\n    return deque->queSize;\n}\n\n/* Check if the double-ended queue is empty */\nbool empty(ArrayDeque *deque) {\n    return deque->queSize == 0;\n}\n\n/* Calculate circular array index */\nint dequeIndex(ArrayDeque *deque, int i) {\n    // Use modulo operation to wrap the array head and tail together\n    // When i exceeds array end, wrap to head\n    // When i passes the head of the array, return to the tail\n    return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* Front of the queue enqueue */\nvoid pushFirst(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Deque is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Use modulo to wrap front from array head to rear\n    deque->front = dequeIndex(deque, deque->front - 1);\n    // Add num to queue front\n    deque->nums[deque->front] = num;\n    deque->queSize++;\n}\n\n/* Rear of the queue enqueue */\nvoid pushLast(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Deque is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    int rear = dequeIndex(deque, deque->front + deque->queSize);\n    // Front pointer moves one position backward\n    deque->nums[rear] = num;\n    deque->queSize++;\n}\n\n/* Return list for printing */\nint peekFirst(ArrayDeque *deque) {\n    // Access error: Deque is empty\n    assert(empty(deque) == 0);\n    return deque->nums[deque->front];\n}\n\n/* Driver Code */\nint peekLast(ArrayDeque *deque) {\n    // Access error: Deque is empty\n    assert(empty(deque) == 0);\n    int last = dequeIndex(deque, deque->front + deque->queSize - 1);\n    return deque->nums[last];\n}\n\n/* Rear of the queue dequeue */\nint popFirst(ArrayDeque *deque) {\n    int num = peekFirst(deque);\n    // Move front pointer backward by one position\n    deque->front = dequeIndex(deque, deque->front + 1);\n    deque->queSize--;\n    return num;\n}\n\n/* Access rear of the queue element */\nint popLast(ArrayDeque *deque) {\n    int num = peekLast(deque);\n    deque->queSize--;\n    return num;\n}\n\n/* Return array for printing */\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
    /* Constructor */\nclass ArrayDeque(capacity: Int) {\n    private var nums: IntArray = IntArray(capacity) // Array for storing double-ended queue elements\n    private var front: Int = 0 // Front pointer, points to the front of the queue element\n    private var queSize: Int = 0 // Double-ended queue length\n\n    /* Get the capacity of the double-ended queue */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Get the length of the double-ended queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the double-ended queue is empty */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Calculate circular array index */\n    private fun index(i: Int): Int {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity()\n    }\n\n    /* Front of the queue enqueue */\n    fun pushFirst(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1)\n        // Add num to front of queue\n        nums[front] = num\n        queSize++\n    }\n\n    /* Rear of the queue enqueue */\n    fun pushLast(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        val rear = index(front + queSize)\n        // Front pointer moves one position backward\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Rear of the queue dequeue */\n    fun popFirst(): Int {\n        val num = peekFirst()\n        // Move front pointer backward by one position\n        front = index(front + 1)\n        queSize--\n        return num\n    }\n\n    /* Access rear of the queue element */\n    fun popLast(): Int {\n        val num = peekLast()\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Driver Code */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Initialize double-ended queue\n        val last = index(front + queSize - 1)\n        return nums[last]\n    }\n\n    /* Return array for printing */\n    fun toArray(): IntArray {\n        // Elements enqueue\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
    ### Deque based on circular array ###\nclass ArrayDeque\n  ### Get deque length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize(capacity)\n    @nums = Array.new(capacity, 0)\n    @front = 0\n    @size = 0\n  end\n\n  ### Get deque capacity ###\n  def capacity\n    @nums.length\n  end\n\n  ### Check if deque is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue at front ###\n  def push_first(num)\n    if size == capacity\n      puts 'Double-ended queue is full'\n      return\n    end\n\n    # Use modulo operation to wrap front around to the tail after passing the head of the array\n    # Add num to the front of the queue\n    @front = index(@front - 1)\n    # Add num to front of queue\n    @nums[@front] = num\n    @size += 1\n  end\n\n  ### Enqueue at rear ###\n  def push_last(num)\n    if size == capacity\n      puts 'Double-ended queue is full'\n      return\n    end\n\n    # Use modulo operation to wrap rear around to the head after passing the tail of the array\n    rear = index(@front + size)\n    # Front pointer moves one position backward\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Dequeue from front ###\n  def pop_first\n    num = peek_first\n    # Move front pointer backward by one position\n    @front = index(@front + 1)\n    @size -= 1\n    num\n  end\n\n  ### Dequeue from rear ###\n  def pop_last\n    num = peek_last\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek_first\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Access rear element ###\n  def peek_last\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    # Initialize double-ended queue\n    last = index(@front + size - 1)\n    @nums[last]\n  end\n\n  ### Return array for printing ###\n  def to_array\n    # Elements enqueue\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  ### Calculate circular array index ###\n  def index(i)\n    # Use modulo operation to wrap the array head and tail together\n    # When i passes the tail of the array, return to the head\n    # When i passes the head of the array, return to the tail\n    (i + capacity) % capacity\n  end\nend\n
    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533-deque-applications","level":2,"title":"5.3.3   Deque Applications","text":"

    A deque combines the logic of both stacks and queues. Therefore, it can implement all application scenarios of both, while providing greater flexibility.

    We know that the \"undo\" function in software is typically implemented using a stack: the system pushes each change operation onto the stack and then implements undo through pop. However, considering system resource limitations, software usually limits the number of undo steps (for example, only allowing 50 steps to be saved). When the stack length exceeds 50, the software needs to perform a deletion operation at the bottom of the stack (front of the queue). But a stack cannot implement this functionality, so a deque is needed to replace the stack. Note that the core logic of \"undo\" still follows the LIFO principle of a stack; it's just that the deque can more flexibly implement some additional logic.

    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   Queue","text":"

    A queue is a linear data structure that follows the First In, First Out (FIFO) rule. As the name suggests, it models people lining up: newcomers continuously join the rear of the queue, while the people at the front leave one by one.

    As shown in Figure 5-4, we call the front of the queue the \"front\" and the end the \"rear.\" The operation of adding an element to the rear is called \"enqueue,\" and the operation of removing the front element is called \"dequeue.\"

    Figure 5-4   FIFO rule of queue

    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521-common-queue-operations","level":2,"title":"5.2.1   Common Queue Operations","text":"

    The common operations on a queue are shown in Table 5-2. Note that method names may vary across programming languages. Here, we use the same naming convention as for stacks.

    Table 5-2   Efficiency of Queue Operations

    Method Description Time Complexity push() Enqueue element, add element to rear \\(O(1)\\) pop() Dequeue front element \\(O(1)\\) peek() Access front element \\(O(1)\\)

    We can directly use the queue classes provided by the programming language:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py
    from collections import deque\n\n# Initialize queue\n# In Python, we generally use the deque class as a queue\n# Although queue.Queue() is a pure queue class, it is not very user-friendly, so it is not recommended\nque: deque[int] = deque()\n\n# Enqueue elements\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# Access front element\nfront: int = que[0]\n\n# Dequeue element\npop: int = que.popleft()\n\n# Get queue length\nsize: int = len(que)\n\n# Check if queue is empty\nis_empty: bool = len(que) == 0\n
    queue.cpp
    /* Initialize queue */\nqueue<int> queue;\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nint front = queue.front();\n\n/* Dequeue element */\nqueue.pop();\n\n/* Get queue length */\nint size = queue.size();\n\n/* Check if queue is empty */\nbool empty = queue.empty();\n
    queue.java
    /* Initialize queue */\nQueue<Integer> queue = new LinkedList<>();\n\n/* Enqueue elements */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* Access front element */\nint peek = queue.peek();\n\n/* Dequeue element */\nint pop = queue.poll();\n\n/* Get queue length */\nint size = queue.size();\n\n/* Check if queue is empty */\nboolean isEmpty = queue.isEmpty();\n
    queue.cs
    /* Initialize queue */\nQueue<int> queue = new();\n\n/* Enqueue elements */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* Access front element */\nint peek = queue.Peek();\n\n/* Dequeue element */\nint pop = queue.Dequeue();\n\n/* Get queue length */\nint size = queue.Count;\n\n/* Check if queue is empty */\nbool isEmpty = queue.Count == 0;\n
    queue_test.go
    /* Initialize queue */\n// In Go, use list as a queue\nqueue := list.New()\n\n/* Enqueue elements */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* Access front element */\npeek := queue.Front()\n\n/* Dequeue element */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* Get queue length */\nsize := queue.Len()\n\n/* Check if queue is empty */\nisEmpty := queue.Len() == 0\n
    queue.swift
    /* Initialize queue */\n// Swift does not have a built-in queue class, can use Array as a queue\nvar queue: [Int] = []\n\n/* Enqueue elements */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* Access front element */\nlet peek = queue.first!\n\n/* Dequeue element */\n// Since it's an array, removeFirst has O(n) complexity\nlet pool = queue.removeFirst()\n\n/* Get queue length */\nlet size = queue.count\n\n/* Check if queue is empty */\nlet isEmpty = queue.isEmpty\n
    queue.js
    /* Initialize queue */\n// JavaScript does not have a built-in queue, can use Array as a queue\nconst queue = [];\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nconst peek = queue[0];\n\n/* Dequeue element */\n// The underlying structure is an array, so shift() has O(n) time complexity\nconst pop = queue.shift();\n\n/* Get queue length */\nconst size = queue.length;\n\n/* Check if queue is empty */\nconst empty = queue.length === 0;\n
    queue.ts
    /* Initialize queue */\n// TypeScript does not have a built-in queue, can use Array as a queue\nconst queue: number[] = [];\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nconst peek = queue[0];\n\n/* Dequeue element */\n// The underlying structure is an array, so shift() has O(n) time complexity\nconst pop = queue.shift();\n\n/* Get queue length */\nconst size = queue.length;\n\n/* Check if queue is empty */\nconst empty = queue.length === 0;\n
    queue.dart
    /* Initialize queue */\n// In Dart, the Queue class is a deque and can also be used as a queue\nQueue<int> queue = Queue();\n\n/* Enqueue elements */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* Access front element */\nint peek = queue.first;\n\n/* Dequeue element */\nint pop = queue.removeFirst();\n\n/* Get queue length */\nint size = queue.length;\n\n/* Check if queue is empty */\nbool isEmpty = queue.isEmpty;\n
    queue.rs
    /* Initialize deque */\n// In Rust, use deque as a regular queue\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Enqueue elements */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* Access front element */\nif let Some(front) = deque.front() {\n}\n\n/* Dequeue element */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* Get queue length */\nlet size = deque.len();\n\n/* Check if queue is empty */\nlet is_empty = deque.is_empty();\n
    queue.c
    // C does not provide a built-in queue\n
    queue.kt
    /* Initialize queue */\nval queue = LinkedList<Int>()\n\n/* Enqueue elements */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* Access front element */\nval peek = queue.peek()\n\n/* Dequeue element */\nval pop = queue.poll()\n\n/* Get queue length */\nval size = queue.size\n\n/* Check if queue is empty */\nval isEmpty = queue.isEmpty()\n
    queue.rb
    # Initialize queue\n# Ruby's built-in queue (Thread::Queue) does not have peek and traversal methods, can use Array as a queue\nqueue = []\n\n# Enqueue elements\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# Access front element\npeek = queue.first\n\n# Dequeue element\n# Please note that since it's an array, Array#shift has O(n) time complexity\npop = queue.shift\n\n# Get queue length\nsize = queue.length\n\n# Check if queue is empty\nis_empty = queue.empty?\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522-queue-implementation","level":2,"title":"5.2.2   Queue Implementation","text":"

    To implement a queue, we need a data structure that allows adding elements at one end and removing elements at the other end. Both linked lists and arrays meet this requirement.

    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1-linked-list-implementation","level":3,"title":"1.   Linked List Implementation","text":"

    As shown in Figure 5-5, we can treat the \"head node\" and \"tail node\" of a linked list as the \"front\" and \"rear\" of the queue, respectively, with the rule that nodes can only be added at the rear and removed from the front.

    <1><2><3>

    Figure 5-5   Enqueue and dequeue operations in linked list implementation of queue

    Below is the code for implementing a queue using a linked list:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py
    class LinkedListQueue:\n    \"\"\"Queue based on linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._front: ListNode | None = None  # Head node front\n        self._rear: ListNode | None = None  # Tail node rear\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Get the length of the queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Enqueue\"\"\"\n        # Add num after the tail node\n        node = ListNode(num)\n        # If the queue is empty, make both front and rear point to the node\n        if self._front is None:\n            self._front = node\n            self._rear = node\n        # If the queue is not empty, add the node after the tail node\n        else:\n            self._rear.next = node\n            self._rear = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Dequeue\"\"\"\n        num = self.peek()\n        # Delete head node\n        self._front = self._front.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Queue is empty\")\n        return self._front.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Convert to list for printing\"\"\"\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n  private:\n    ListNode *front, *rear; // Head node front, tail node rear\n    int queSize;\n\n  public:\n    LinkedListQueue() {\n        front = nullptr;\n        rear = nullptr;\n        queSize = 0;\n    }\n\n    ~LinkedListQueue() {\n        // Traverse linked list to delete nodes and free memory\n        freeMemoryLinkedList(front);\n    }\n\n    /* Get the length of the queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    void push(int num) {\n        // Add num after the tail node\n        ListNode *node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == nullptr) {\n            front = node;\n            rear = node;\n        }\n        // If the queue is not empty, add the node after the tail node\n        else {\n            rear->next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    int pop() {\n        int num = peek();\n        // Delete head node\n        ListNode *tmp = front;\n        front = front->next;\n        // Free memory\n        delete tmp;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peek() {\n        if (size() == 0)\n            throw out_of_range(\"Queue is empty\");\n        return front->val;\n    }\n\n    /* Convert linked list to Vector and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private ListNode front, rear; // Head node front, tail node rear\n    private int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue */\n    public void push(int num) {\n        // Add num after the tail node\n        ListNode node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node;\n            rear = node;\n        // If the queue is not empty, add the node after the tail node\n        } else {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int pop() {\n        int num = peek();\n        // Delete head node\n        front = front.next;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    ListNode? front, rear;  // Head node front, tail node rear\n    int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Enqueue */\n    public void Push(int num) {\n        // Add num after the tail node\n        ListNode node = new(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node;\n            rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else if (rear != null) {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int Pop() {\n        int num = Peek();\n        // Delete head node\n        front = front?.next;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return front!.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\ntype linkedListQueue struct {\n    // Use built-in package list to implement queue\n    data *list.List\n}\n\n/* Access front of the queue element */\nfunc newLinkedListQueue() *linkedListQueue {\n    return &linkedListQueue{\n        data: list.New(),\n    }\n}\n\n/* Enqueue */\nfunc (s *linkedListQueue) push(value any) {\n    s.data.PushBack(value)\n}\n\n/* Dequeue */\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/* Return list for printing */\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/* Get the length of the queue */\nfunc (s *linkedListQueue) size() int {\n    return s.data.Len()\n}\n\n/* Check if the queue is empty */\nfunc (s *linkedListQueue) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListQueue) toList() *list.List {\n    return s.data\n}\n
    linkedlist_queue.swift
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private var front: ListNode? // Head node\n    private var rear: ListNode? // Tail node\n    private var _size: Int\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue */\n    func push(num: Int) {\n        // Add num after the tail node\n        let node = ListNode(x: num)\n        // If the queue is empty, make both front and rear point to the node\n        if front == nil {\n            front = node\n            rear = node\n        }\n        // If the queue is not empty, add the node after the tail node\n        else {\n            rear?.next = node\n            rear = node\n        }\n        _size += 1\n    }\n\n    /* Dequeue */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Delete head node\n        front = front?.next\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Queue is empty\")\n        }\n        return front!.val\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    #front; // Front node #front\n    #rear; // Rear node #rear\n    #queSize = 0;\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n    }\n\n    /* Get the length of the queue */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Enqueue */\n    push(num) {\n        // Add num after the tail node\n        const node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (!this.#front) {\n            this.#front = node;\n            this.#rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            this.#rear.next = node;\n            this.#rear = node;\n        }\n        this.#queSize++;\n    }\n\n    /* Dequeue */\n    pop() {\n        const num = this.peek();\n        // Delete head node\n        this.#front = this.#front.next;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (this.size === 0) throw new Error('Queue is empty');\n        return this.#front.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private front: ListNode | null; // Head node front\n    private rear: ListNode | null; // Tail node rear\n    private queSize: number = 0;\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n    }\n\n    /* Get the length of the queue */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Enqueue */\n    push(num: number): void {\n        // Add num after the tail node\n        const node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (!this.front) {\n            this.front = node;\n            this.rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            this.rear!.next = node;\n            this.rear = node;\n        }\n        this.queSize++;\n    }\n\n    /* Dequeue */\n    pop(): number {\n        const num = this.peek();\n        if (!this.front) throw new Error('Queue is empty');\n        // Delete head node\n        this.front = this.front.next;\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (this.size === 0) throw new Error('Queue is empty');\n        return this.front!.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n  ListNode? _front; // Head node _front\n  ListNode? _rear; // Tail node _rear\n  int _queSize = 0; // Queue length\n\n  LinkedListQueue() {\n    _front = null;\n    _rear = null;\n  }\n\n  /* Get the length of the queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Enqueue */\n  void push(int _num) {\n    // Add _num after tail node\n    final node = ListNode(_num);\n    // If the queue is empty, make both front and rear point to the node\n    if (_front == null) {\n      _front = node;\n      _rear = node;\n    } else {\n      // If the queue is not empty, add the node after the tail node\n      _rear!.next = node;\n      _rear = node;\n    }\n    _queSize++;\n  }\n\n  /* Dequeue */\n  int pop() {\n    final int _num = peek();\n    // Delete head node\n    _front = _front!.next;\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (_queSize == 0) {\n      throw Exception('Queue is empty');\n    }\n    return _front!.val;\n  }\n\n  /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListQueue<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Head node front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Tail node rear\n    que_size: usize,                         // Queue length\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    /* Get the length of the queue */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Check if the queue is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Enqueue */\n    pub fn push(&mut self, num: T) {\n        // Add num after the tail node\n        let new_rear = ListNode::new(num);\n        match self.rear.take() {\n            // If the queue is not empty, add the node after the tail node\n            Some(old_rear) => {\n                old_rear.borrow_mut().next = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n            // If the queue is empty, make both front and rear point to the node\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    /* Dequeue */\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    /* Return list for printing */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\ntypedef struct {\n    ListNode *front, *rear;\n    int queSize;\n} LinkedListQueue;\n\n/* Constructor */\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/* Destructor */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n    // Free all nodes\n    while (queue->front != NULL) {\n        ListNode *tmp = queue->front;\n        queue->front = queue->front->next;\n        free(tmp);\n    }\n    // Free queue structure\n    free(queue);\n}\n\n/* Get the length of the queue */\nint size(LinkedListQueue *queue) {\n    return queue->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(LinkedListQueue *queue) {\n    return (size(queue) == 0);\n}\n\n/* Enqueue */\nvoid push(LinkedListQueue *queue, int num) {\n    // Add node at tail\n    ListNode *node = newListNode(num);\n    // If the queue is empty, make both front and rear point to the node\n    if (queue->front == NULL) {\n        queue->front = node;\n        queue->rear = node;\n    }\n    // If the queue is not empty, add the node after the tail node\n    else {\n        queue->rear->next = node;\n        queue->rear = node;\n    }\n    queue->queSize++;\n}\n\n/* Return list for printing */\nint peek(LinkedListQueue *queue) {\n    assert(size(queue) && queue->front);\n    return queue->front->val;\n}\n\n/* Dequeue */\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/* Print queue */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n    int *arr = malloc(sizeof(int) * queue->queSize);\n    // Copy data from list to array\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue(\n    // Head node front, tail node rear\n    private var front: ListNode? = null,\n    private var rear: ListNode? = null,\n    private var queSize: Int = 0\n) {\n\n    /* Get the length of the queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the queue is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Enqueue */\n    fun push(num: Int) {\n        // Add num after the tail node\n        val node = ListNode(num)\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node\n            rear = node\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            rear?.next = node\n            rear = node\n        }\n        queSize++\n    }\n\n    /* Dequeue */\n    fun pop(): Int {\n        val num = peek()\n        // Delete head node\n        front = front?.next\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Convert linked list to Array and return */\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
    ### Queue based on linked list ###\nclass LinkedListQueue\n  ### Get queue length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @front = nil  # Head node front\n    @rear = nil   # Tail node rear\n    @size = 0\n  end\n\n  ### Check if queue is empty ###\n  def is_empty?\n    @front.nil?\n  end\n\n  ### Enqueue ###\n  def push(num)\n    # Add num after the tail node\n    node = ListNode.new(num)\n\n    # If queue is empty, set both front and rear to this node\n    if @front.nil?\n      @front = node\n      @rear = node\n    # If queue is not empty, add this node after rear\n    else\n      @rear.next = node\n      @rear = node\n    end\n\n    @size += 1\n  end\n\n  ### Dequeue ###\n  def pop\n    num = peek\n    # Delete head node\n    @front = @front.next\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek\n    raise IndexError, 'Queue is empty' if is_empty?\n\n    @front.val\n  end\n\n  ### Convert linked list to Array and return ###\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":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

    Deleting the first element in an array has a time complexity of \\(O(n)\\), which would make the dequeue operation inefficient. However, we can use the following clever method to avoid this problem.

    We can use a variable front to point to the index of the front element and maintain a variable size to record the queue length. We define rear = front + size, which calculates the position right after the rear element.

    Based on this design, the valid interval containing elements in the array is [front, rear - 1]. The implementation methods for various operations are shown in Figure 5-6:

    • Enqueue operation: Assign the input element to the rear index and increase size by 1.
    • Dequeue operation: Simply increase front by 1 and decrease size by 1.

    As you can see, both enqueue and dequeue operations require only one operation, with a time complexity of \\(O(1)\\).

    <1><2><3>

    Figure 5-6   Enqueue and dequeue operations in array implementation of queue

    You may notice a problem: as we continuously enqueue and dequeue, both front and rear move to the right. When they reach the end of the array, they cannot continue moving. To solve this problem, we can treat the array as a \"circular array\" with head and tail connected.

    For a circular array, we need to let front or rear wrap around to the beginning of the array when they cross the end. This periodic pattern can be implemented using the \"modulo operation,\" as shown in the code below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py
    class ArrayQueue:\n    \"\"\"Queue based on circular array implementation\"\"\"\n\n    def __init__(self, size: int):\n        \"\"\"Constructor\"\"\"\n        self._nums: list[int] = [0] * size  # Array for storing queue elements\n        self._front: int = 0  # Front pointer, points to the front of the queue element\n        self._size: int = 0  # Queue length\n\n    def capacity(self) -> int:\n        \"\"\"Get the capacity of the queue\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Get the length of the queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Enqueue\"\"\"\n        if self._size == self.capacity():\n            raise IndexError(\"Queue is full\")\n        # Calculate rear pointer, points to rear index + 1\n        # Use modulo operation to wrap rear around to the head after passing the tail of the array\n        rear: int = (self._front + self._size) % self.capacity()\n        # Add num to the rear of the queue\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Dequeue\"\"\"\n        num: int = self.peek()\n        # Front pointer moves one position backward, if it passes the tail, return to the head of the array\n        self._front = (self._front + 1) % self.capacity()\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Queue is empty\")\n        return self._nums[self._front]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Return list for printing\"\"\"\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n  private:\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Queue length\n    int queCapacity; // Queue capacity\n\n  public:\n    ArrayQueue(int capacity) {\n        // Initialize array\n        nums = new int[capacity];\n        queCapacity = capacity;\n        front = queSize = 0;\n    }\n\n    ~ArrayQueue() {\n        delete[] nums;\n    }\n\n    /* Get the capacity of the queue */\n    int capacity() {\n        return queCapacity;\n    }\n\n    /* Get the length of the queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue */\n    void push(int num) {\n        if (queSize == queCapacity) {\n            cout << \"Queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % queCapacity;\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    int pop() {\n        int num = peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % queCapacity;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peek() {\n        if (isEmpty())\n            throw out_of_range(\"Queue is empty\");\n        return nums[front];\n    }\n\n    /* Convert array to Vector and return */\n    vector<int> toVector() {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    private int[] nums; // Array for storing queue elements\n    private int front; // Front pointer, points to the front of the queue element\n    private int queSize; // Queue length\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Get the length of the queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    public void push(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % capacity();\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int pop() {\n        int num = peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Return array */\n    public int[] toArray() {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    int[] nums;  // Array for storing queue elements\n    int front;   // Front pointer, points to the front of the queue element\n    int queSize; // Queue length\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Get the length of the queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    public void Push(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % Capacity();\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int Pop() {\n        int num = Peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % Capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return nums[front];\n    }\n\n    /* Return array */\n    public int[] ToArray() {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\ntype arrayQueue struct {\n    nums        []int // Array for storing queue elements\n    front       int   // Front pointer, points to the front of the queue element\n    queSize     int   // Queue length\n    queCapacity int   // Queue capacity (maximum number of elements)\n}\n\n/* Access front of the queue element */\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/* Get the length of the queue */\nfunc (q *arrayQueue) size() int {\n    return q.queSize\n}\n\n/* Check if the queue is empty */\nfunc (q *arrayQueue) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Enqueue */\nfunc (q *arrayQueue) push(num int) {\n    // When rear == queCapacity, queue is full\n    if q.queSize == q.queCapacity {\n        return\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    rear := (q.front + q.queSize) % q.queCapacity\n    // Front pointer moves one position backward\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Dequeue */\nfunc (q *arrayQueue) pop() any {\n    num := q.peek()\n    if num == nil {\n        return nil\n    }\n\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    q.front = (q.front + 1) % q.queCapacity\n    q.queSize--\n    return num\n}\n\n/* Return list for printing */\nfunc (q *arrayQueue) peek() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Get Slice for printing */\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    private var nums: [Int] // Array for storing queue elements\n    private var front: Int // Front pointer, points to the front of the queue element\n    private var _size: Int // Queue length\n\n    init(capacity: Int) {\n        // Initialize array\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Get the capacity of the queue */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Get the length of the queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue */\n    func push(num: Int) {\n        if size() == capacity() {\n            print(\"Queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        let rear = (front + size()) % capacity()\n        // Front pointer moves one position backward\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Dequeue */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity()\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Queue is empty\")\n        }\n        return nums[front]\n    }\n\n    /* Return array */\n    func toArray() -> [Int] {\n        // Elements enqueue\n        (front ..< front + size()).map { nums[$0 % capacity()] }\n    }\n}\n
    array_queue.js
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    #nums; // Array for storing queue elements\n    #front = 0; // Front pointer, points to the front of the queue element\n    #queSize = 0; // Queue length\n\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n    }\n\n    /* Get the capacity of the queue */\n    get capacity() {\n        return this.#nums.length;\n    }\n\n    /* Get the length of the queue */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Enqueue */\n    push(num) {\n        if (this.size === this.capacity) {\n            console.log('Queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        const rear = (this.#front + this.size) % this.capacity;\n        // Front pointer moves one position backward\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Dequeue */\n    pop() {\n        const num = this.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        this.#front = (this.#front + 1) % this.capacity;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (this.isEmpty()) throw new Error('Queue is empty');\n        return this.#nums[this.#front];\n    }\n\n    /* Return Array */\n    toArray() {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    private nums: number[]; // Array for storing queue elements\n    private front: number; // Front pointer, points to the front of the queue element\n    private queSize: number; // Queue length\n\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = this.queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    get capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Get the length of the queue */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Enqueue */\n    push(num: number): void {\n        if (this.size === this.capacity) {\n            console.log('Queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        const rear = (this.front + this.queSize) % this.capacity;\n        // Front pointer moves one position backward\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Dequeue */\n    pop(): number {\n        const num = this.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        this.front = (this.front + 1) % this.capacity;\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (this.isEmpty()) throw new Error('Queue is empty');\n        return this.nums[this.front];\n    }\n\n    /* Return Array */\n    toArray(): number[] {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n  late List<int> _nums; // Array for storing queue elements\n  late int _front; // Front pointer, points to the front of the queue element\n  late int _queSize; // Queue length\n\n  ArrayQueue(int capacity) {\n    _nums = List.filled(capacity, 0);\n    _front = _queSize = 0;\n  }\n\n  /* Get the capacity of the queue */\n  int capaCity() {\n    return _nums.length;\n  }\n\n  /* Get the length of the queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Enqueue */\n  void push(int _num) {\n    if (_queSize == capaCity()) {\n      throw Exception(\"Queue is full\");\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    int rear = (_front + _queSize) % capaCity();\n    // Add _num to queue rear\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Dequeue */\n  int pop() {\n    int _num = peek();\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    _front = (_front + 1) % capaCity();\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"Queue is empty\");\n    }\n    return _nums[_front];\n  }\n\n  /* Return Array */\n  List<int> toArray() {\n    // Elements enqueue\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
    /* Queue based on circular array implementation */\nstruct ArrayQueue<T> {\n    nums: Vec<T>,      // Array for storing queue elements\n    front: i32,        // Front pointer, points to the front of the queue element\n    que_size: i32,     // Queue length\n    que_capacity: i32, // Queue capacity\n}\n\nimpl<T: Copy + Default> ArrayQueue<T> {\n    /* Constructor */\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    /* Get the capacity of the queue */\n    fn capacity(&self) -> i32 {\n        self.que_capacity\n    }\n\n    /* Get the length of the queue */\n    fn size(&self) -> i32 {\n        self.que_size\n    }\n\n    /* Check if the queue is empty */\n    fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Enqueue */\n    fn push(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        let rear = (self.front + self.que_size) % self.que_capacity;\n        // Front pointer moves one position backward\n        self.nums[rear as usize] = num;\n        self.que_size += 1;\n    }\n\n    /* Dequeue */\n    fn pop(&mut self) -> T {\n        let num = self.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        self.front = (self.front + 1) % self.que_capacity;\n        self.que_size -= 1;\n        num\n    }\n\n    /* Return list for printing */\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    /* Return array */\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
    /* Queue based on circular array implementation */\ntypedef struct {\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Current number of elements in the queue\n    int queCapacity; // Queue capacity\n} ArrayQueue;\n\n/* Constructor */\nArrayQueue *newArrayQueue(int capacity) {\n    ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n    // Initialize array\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/* Destructor */\nvoid delArrayQueue(ArrayQueue *queue) {\n    free(queue->nums);\n    free(queue);\n}\n\n/* Get the capacity of the queue */\nint capacity(ArrayQueue *queue) {\n    return queue->queCapacity;\n}\n\n/* Get the length of the queue */\nint size(ArrayQueue *queue) {\n    return queue->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(ArrayQueue *queue) {\n    return queue->queSize == 0;\n}\n\n/* Return list for printing */\nint peek(ArrayQueue *queue) {\n    assert(size(queue) != 0);\n    return queue->nums[queue->front];\n}\n\n/* Enqueue */\nvoid push(ArrayQueue *queue, int num) {\n    if (size(queue) == capacity(queue)) {\n        printf(\"Queue is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    int rear = (queue->front + queue->queSize) % queue->queCapacity;\n    // Front pointer moves one position backward\n    queue->nums[rear] = num;\n    queue->queSize++;\n}\n\n/* Dequeue */\nint pop(ArrayQueue *queue) {\n    int num = peek(queue);\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    queue->front = (queue->front + 1) % queue->queCapacity;\n    queue->queSize--;\n    return num;\n}\n\n/* Return array for printing */\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
    /* Queue based on circular array implementation */\nclass ArrayQueue(capacity: Int) {\n    private val nums: IntArray = IntArray(capacity) // Array for storing queue elements\n    private var front: Int = 0 // Front pointer, points to the front of the queue element\n    private var queSize: Int = 0 // Queue length\n\n    /* Get the capacity of the queue */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Get the length of the queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the queue is empty */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Enqueue */\n    fun push(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        val rear = (front + queSize) % capacity()\n        // Front pointer moves one position backward\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Dequeue */\n    fun pop(): Int {\n        val num = peek()\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity()\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Return array */\n    fun toArray(): IntArray {\n        // Elements enqueue\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
    ### Queue based on circular array ###\nclass ArrayQueue\n  ### Get queue length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize(size)\n    @nums = Array.new(size, 0) # Array for storing queue elements\n    @front = 0 # Front pointer, points to the front of the queue element\n    @size = 0 # Queue length\n  end\n\n  ### Get queue capacity ###\n  def capacity\n    @nums.length\n  end\n\n  ### Check if queue is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue ###\n  def push(num)\n    raise IndexError, 'Queue is full' if size == capacity\n\n    # Use modulo operation to wrap rear around to the head after passing the tail of the array\n    # Add num to the rear of the queue\n    rear = (@front + size) % capacity\n    # Front pointer moves one position backward\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Dequeue ###\n  def pop\n    num = peek\n    # Move front pointer backward by one position, if it passes the tail, return to array head\n    @front = (@front + 1) % capacity\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek\n    raise IndexError, 'Queue is empty' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Return list for printing ###\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

    The queue implemented above still has limitations: its length is immutable. However, this problem is not difficult to solve. We can replace the array with a dynamic array to introduce an expansion mechanism. Interested readers can try to implement this themselves.

    The comparison conclusions for the two implementations are consistent with those for stacks and will not be repeated here.

    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523-typical-applications-of-queue","level":2,"title":"5.2.3   Typical Applications of Queue","text":"
    • Taobao orders. After shoppers place orders, the orders are added to a queue, and the system subsequently processes the orders in the queue according to their sequence. During Double Eleven, massive orders are generated in a short time, and high concurrency becomes a key challenge that engineers need to tackle.
    • Various to-do tasks. Any scenario that needs to implement \"first come, first served\" functionality, such as a printer's task queue or a restaurant's order queue, can effectively maintain the processing order using queues.
    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   Stack","text":"

    A stack is a linear data structure that follows the Last In, First Out (LIFO) principle.

    We can compare a stack to a pile of plates on a table. If we specify that only one plate can be moved at a time, then to get the bottom plate, we must first remove the plates above it one by one. If we replace the plates with various types of elements (such as integers, characters, objects, etc.), we get the stack data structure.

    As shown in Figure 5-1, we call the top of the stacked elements the \"top\" and the bottom the \"bottom.\" The operation of adding an element to the top is called \"push,\" and the operation of removing the top element is called \"pop.\"

    Figure 5-1   LIFO rule of stack

    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511-common-stack-operations","level":2,"title":"5.1.1   Common Stack Operations","text":"

    The common operations on a stack are shown in Table 5-1. The specific method names depend on the programming language used. Here, we use the common naming convention of push(), pop(), and peek().

    Table 5-1   Efficiency of Stack Operations

    Method Description Time Complexity push() Push element onto stack (add to top) \\(O(1)\\) pop() Pop top element from stack \\(O(1)\\) peek() Access top element \\(O(1)\\)

    Typically, we can directly use the built-in stack class provided by the programming language. However, some languages may not provide a dedicated stack class. In such cases, we can use the language's \"array\" or \"linked list\" as a stack and simply avoid using operations unrelated to stack behavior.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py
    # Initialize stack\n# Python does not have a built-in stack class, can use list as a stack\nstack: list[int] = []\n\n# Push elements\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# Access top element\npeek: int = stack[-1]\n\n# Pop element\npop: int = stack.pop()\n\n# Get stack length\nsize: int = len(stack)\n\n# Check if empty\nis_empty: bool = len(stack) == 0\n
    stack.cpp
    /* Initialize stack */\nstack<int> stack;\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nint top = stack.top();\n\n/* Pop element */\nstack.pop(); // No return value\n\n/* Get stack length */\nint size = stack.size();\n\n/* Check if empty */\nbool empty = stack.empty();\n
    stack.java
    /* Initialize stack */\nStack<Integer> stack = new Stack<>();\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nint peek = stack.peek();\n\n/* Pop element */\nint pop = stack.pop();\n\n/* Get stack length */\nint size = stack.size();\n\n/* Check if empty */\nboolean isEmpty = stack.isEmpty();\n
    stack.cs
    /* Initialize stack */\nStack<int> stack = new();\n\n/* Push elements */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* Access top element */\nint peek = stack.Peek();\n\n/* Pop element */\nint pop = stack.Pop();\n\n/* Get stack length */\nint size = stack.Count;\n\n/* Check if empty */\nbool isEmpty = stack.Count == 0;\n
    stack_test.go
    /* Initialize stack */\n// In Go, it is recommended to use Slice as a stack\nvar stack []int\n\n/* Push elements */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* Access top element */\npeek := stack[len(stack)-1]\n\n/* Pop element */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* Get stack length */\nsize := len(stack)\n\n/* Check if empty */\nisEmpty := len(stack) == 0\n
    stack.swift
    /* Initialize stack */\n// Swift does not have a built-in stack class, can use Array as a stack\nvar stack: [Int] = []\n\n/* Push elements */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* Access top element */\nlet peek = stack.last!\n\n/* Pop element */\nlet pop = stack.removeLast()\n\n/* Get stack length */\nlet size = stack.count\n\n/* Check if empty */\nlet isEmpty = stack.isEmpty\n
    stack.js
    /* Initialize stack */\n// JavaScript does not have a built-in stack class, can use Array as a stack\nconst stack = [];\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nconst peek = stack[stack.length-1];\n\n/* Pop element */\nconst pop = stack.pop();\n\n/* Get stack length */\nconst size = stack.length;\n\n/* Check if empty */\nconst is_empty = stack.length === 0;\n
    stack.ts
    /* Initialize stack */\n// TypeScript does not have a built-in stack class, can use Array as a stack\nconst stack: number[] = [];\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nconst peek = stack[stack.length - 1];\n\n/* Pop element */\nconst pop = stack.pop();\n\n/* Get stack length */\nconst size = stack.length;\n\n/* Check if empty */\nconst is_empty = stack.length === 0;\n
    stack.dart
    /* Initialize stack */\n// Dart does not have a built-in stack class, can use List as a stack\nList<int> stack = [];\n\n/* Push elements */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* Access top element */\nint peek = stack.last;\n\n/* Pop element */\nint pop = stack.removeLast();\n\n/* Get stack length */\nint size = stack.length;\n\n/* Check if empty */\nbool isEmpty = stack.isEmpty;\n
    stack.rs
    /* Initialize stack */\n// Use Vec as a stack\nlet mut stack: Vec<i32> = Vec::new();\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nlet top = stack.last().unwrap();\n\n/* Pop element */\nlet pop = stack.pop().unwrap();\n\n/* Get stack length */\nlet size = stack.len();\n\n/* Check if empty */\nlet is_empty = stack.is_empty();\n
    stack.c
    // C does not provide a built-in stack\n
    stack.kt
    /* Initialize stack */\nval stack = Stack<Int>()\n\n/* Push elements */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* Access top element */\nval peek = stack.peek()\n\n/* Pop element */\nval pop = stack.pop()\n\n/* Get stack length */\nval size = stack.size\n\n/* Check if empty */\nval isEmpty = stack.isEmpty()\n
    stack.rb
    # Initialize stack\n# Ruby does not have a built-in stack class, can use Array as a stack\nstack = []\n\n# Push elements\nstack << 1\nstack << 3\nstack << 2\nstack << 5\nstack << 4\n\n# Access top element\npeek = stack.last\n\n# Pop element\npop = stack.pop\n\n# Get stack length\nsize = stack.length\n\n# Check if empty\nis_empty = stack.empty?\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512-stack-implementation","level":2,"title":"5.1.2   Stack Implementation","text":"

    To gain a deeper understanding of how a stack operates, let's try implementing a stack class ourselves.

    A stack follows the LIFO principle, so we can only add or remove elements at the top. However, both arrays and linked lists allow adding and removing elements at any position. Therefore, a stack can be viewed as a restricted array or linked list. In other words, we can \"shield\" some irrelevant operations of arrays or linked lists so that their external logic conforms to the characteristics of a stack.

    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1-linked-list-implementation","level":3,"title":"1.   Linked List Implementation","text":"

    When implementing a stack using a linked list, we can treat the head node of the linked list as the top of the stack and the tail node as the base.

    As shown in Figure 5-2, for the push operation, we simply insert an element at the head of the linked list. This node insertion method is called the \"head insertion method.\" For the pop operation, we just need to remove the head node from the linked list.

    <1><2><3>

    Figure 5-2   Push and pop operations in linked list implementation of stack

    Below is sample code for implementing a stack based on a linked list:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py
    class LinkedListStack:\n    \"\"\"Stack based on linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._peek: ListNode | None = None\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Get the length of the stack\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the stack is empty\"\"\"\n        return self._size == 0\n\n    def push(self, val: int):\n        \"\"\"Push\"\"\"\n        node = ListNode(val)\n        node.next = self._peek\n        self._peek = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Pop\"\"\"\n        num = self.peek()\n        self._peek = self._peek.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access top of the stack element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._peek.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Convert to list for printing\"\"\"\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n  private:\n    ListNode *stackTop; // Use head node as stack top\n    int stkSize;        // Stack length\n\n  public:\n    LinkedListStack() {\n        stackTop = nullptr;\n        stkSize = 0;\n    }\n\n    ~LinkedListStack() {\n        // Traverse linked list to delete nodes and free memory\n        freeMemoryLinkedList(stackTop);\n    }\n\n    /* Get the length of the stack */\n    int size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    void push(int num) {\n        ListNode *node = new ListNode(num);\n        node->next = stackTop;\n        stackTop = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    int pop() {\n        int num = top();\n        ListNode *tmp = stackTop;\n        stackTop = stackTop->next;\n        // Free memory\n        delete tmp;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"Stack is empty\");\n        return stackTop->val;\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    private ListNode stackPeek; // Use head node as stack top\n    private int stkSize = 0; // Stack length\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    public int size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    public void push(int num) {\n        ListNode node = new ListNode(num);\n        node.next = stackPeek;\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    public int pop() {\n        int num = peek();\n        stackPeek = stackPeek.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stackPeek.val;\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    ListNode? stackPeek;  // Use head node as stack top\n    int stkSize = 0;   // Stack length\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    public int Size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Push */\n    public void Push(int num) {\n        ListNode node = new(num) {\n            next = stackPeek\n        };\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    public int Pop() {\n        int num = Peek();\n        stackPeek = stackPeek!.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stackPeek!.val;\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\ntype linkedListStack struct {\n    // Use built-in package list to implement stack\n    data *list.List\n}\n\n/* Access top of the stack element */\nfunc newLinkedListStack() *linkedListStack {\n    return &linkedListStack{\n        data: list.New(),\n    }\n}\n\n/* Push */\nfunc (s *linkedListStack) push(value int) {\n    s.data.PushBack(value)\n}\n\n/* Pop */\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/* Return list for printing */\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/* Get the length of the stack */\nfunc (s *linkedListStack) size() int {\n    return s.data.Len()\n}\n\n/* Check if the stack is empty */\nfunc (s *linkedListStack) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListStack) toList() *list.List {\n    return s.data\n}\n
    linkedlist_stack.swift
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    private var _peek: ListNode? // Use head node as stack top\n    private var _size: Int // Stack length\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the stack */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the stack is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Push */\n    func push(num: Int) {\n        let node = ListNode(x: num)\n        node.next = _peek\n        _peek = node\n        _size += 1\n    }\n\n    /* Pop */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        _peek = _peek?.next\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return _peek!.val\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    #stackPeek; // Use head node as stack top\n    #stkSize = 0; // Stack length\n\n    constructor() {\n        this.#stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    get size() {\n        return this.#stkSize;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Push */\n    push(num) {\n        const node = new ListNode(num);\n        node.next = this.#stackPeek;\n        this.#stackPeek = node;\n        this.#stkSize++;\n    }\n\n    /* Pop */\n    pop() {\n        const num = this.peek();\n        this.#stackPeek = this.#stackPeek.next;\n        this.#stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (!this.#stackPeek) throw new Error('Stack is empty');\n        return this.#stackPeek.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    private stackPeek: ListNode | null; // Use head node as stack top\n    private stkSize: number = 0; // Stack length\n\n    constructor() {\n        this.stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    get size(): number {\n        return this.stkSize;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Push */\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    /* Pop */\n    pop(): number {\n        const num = this.peek();\n        if (!this.stackPeek) throw new Error('Stack is empty');\n        this.stackPeek = this.stackPeek.next;\n        this.stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (!this.stackPeek) throw new Error('Stack is empty');\n        return this.stackPeek.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Stack implemented based on linked list class */\nclass LinkedListStack {\n  ListNode? _stackPeek; // Use head node as stack top\n  int _stkSize = 0; // Stack length\n\n  LinkedListStack() {\n    _stackPeek = null;\n  }\n\n  /* Get the length of the stack */\n  int size() {\n    return _stkSize;\n  }\n\n  /* Check if the stack is empty */\n  bool isEmpty() {\n    return _stkSize == 0;\n  }\n\n  /* Push */\n  void push(int _num) {\n    final ListNode node = ListNode(_num);\n    node.next = _stackPeek;\n    _stackPeek = node;\n    _stkSize++;\n  }\n\n  /* Pop */\n  int pop() {\n    final int _num = peek();\n    _stackPeek = _stackPeek!.next;\n    _stkSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (_stackPeek == null) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stackPeek!.val;\n  }\n\n  /* Convert linked list to List and return */\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
    /* Stack based on linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListStack<T> {\n    stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // Use head node as stack top\n    stk_size: usize,                              // Stack length\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    /* Get the length of the stack */\n    pub fn size(&self) -> usize {\n        return self.stk_size;\n    }\n\n    /* Check if the stack is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.size() == 0;\n    }\n\n    /* Push */\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    /* Pop */\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    /* Return list for printing */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.stack_peek.as_ref()\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\ntypedef struct {\n    ListNode *top; // Use head node as stack top\n    int size;      // Stack length\n} LinkedListStack;\n\n/* Constructor */\nLinkedListStack *newLinkedListStack() {\n    LinkedListStack *s = malloc(sizeof(LinkedListStack));\n    s->top = NULL;\n    s->size = 0;\n    return s;\n}\n\n/* Destructor */\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/* Get the length of the stack */\nint size(LinkedListStack *s) {\n    return s->size;\n}\n\n/* Check if the stack is empty */\nbool isEmpty(LinkedListStack *s) {\n    return size(s) == 0;\n}\n\n/* Push */\nvoid push(LinkedListStack *s, int num) {\n    ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n    node->next = s->top; // Update new node's pointer field\n    node->val = num;     // Update new node's data field\n    s->top = node;       // Update stack top\n    s->size++;           // Update stack size\n}\n\n/* Return list for printing */\nint peek(LinkedListStack *s) {\n    if (s->size == 0) {\n        printf(\"Stack is empty\\n\");\n        return INT_MAX;\n    }\n    return s->top->val;\n}\n\n/* Pop */\nint pop(LinkedListStack *s) {\n    int val = peek(s);\n    ListNode *tmp = s->top;\n    s->top = s->top->next;\n    // Free memory\n    free(tmp);\n    s->size--;\n    return val;\n}\n
    linkedlist_stack.kt
    /* Stack based on linked list implementation */\nclass LinkedListStack(\n    private var stackPeek: ListNode? = null, // Use head node as stack top\n    private var stkSize: Int = 0 // Stack length\n) {\n\n    /* Get the length of the stack */\n    fun size(): Int {\n        return stkSize\n    }\n\n    /* Check if the stack is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Push */\n    fun push(num: Int) {\n        val node = ListNode(num)\n        node.next = stackPeek\n        stackPeek = node\n        stkSize++\n    }\n\n    /* Pop */\n    fun pop(): Int? {\n        val num = peek()\n        stackPeek = stackPeek?.next\n        stkSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int? {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stackPeek?._val\n    }\n\n    /* Convert List to Array and return */\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
    ### Stack based on linked list ###\nclass LinkedListStack\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @size = 0\n  end\n\n  ### Check if stack is empty ###\n  def is_empty?\n    @peek.nil?\n  end\n\n  ### Push ###\n  def push(val)\n    node = ListNode.new(val)\n    node.next = @peek\n    @peek = node\n    @size += 1\n  end\n\n  ### Pop ###\n  def pop\n    num = peek\n    @peek = @peek.next\n    @size -= 1\n    num\n  end\n\n  ### Access top element ###\n  def peek\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @peek.val\n  end\n\n  ### Convert linked list to Array and return ###\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":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

    When implementing a stack using an array, we can treat the end of the array as the top of the stack. As shown in Figure 5-3, push and pop operations correspond to adding and removing elements at the end of the array, both with a time complexity of \\(O(1)\\).

    <1><2><3>

    Figure 5-3   Push and pop operations in array implementation of stack

    Since elements pushed onto the stack may increase continuously, we can use a dynamic array, which eliminates the need to handle array expansion ourselves. Here is the sample code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py
    class ArrayStack:\n    \"\"\"Stack based on array implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._stack: list[int] = []\n\n    def size(self) -> int:\n        \"\"\"Get the length of the stack\"\"\"\n        return len(self._stack)\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the stack is empty\"\"\"\n        return self.size() == 0\n\n    def push(self, item: int):\n        \"\"\"Push\"\"\"\n        self._stack.append(item)\n\n    def pop(self) -> int:\n        \"\"\"Pop\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._stack.pop()\n\n    def peek(self) -> int:\n        \"\"\"Access top of the stack element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._stack[-1]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Return list for printing\"\"\"\n        return self._stack\n
    array_stack.cpp
    /* Stack based on array implementation */\nclass ArrayStack {\n  private:\n    vector<int> stack;\n\n  public:\n    /* Get the length of the stack */\n    int size() {\n        return stack.size();\n    }\n\n    /* Check if the stack is empty */\n    bool isEmpty() {\n        return stack.size() == 0;\n    }\n\n    /* Push */\n    void push(int num) {\n        stack.push_back(num);\n    }\n\n    /* Pop */\n    int pop() {\n        int num = top();\n        stack.pop_back();\n        return num;\n    }\n\n    /* Return list for printing */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"Stack is empty\");\n        return stack.back();\n    }\n\n    /* Return Vector */\n    vector<int> toVector() {\n        return stack;\n    }\n};\n
    array_stack.java
    /* Stack based on array implementation */\nclass ArrayStack {\n    private ArrayList<Integer> stack;\n\n    public ArrayStack() {\n        // Initialize list (dynamic array)\n        stack = new ArrayList<>();\n    }\n\n    /* Get the length of the stack */\n    public int size() {\n        return stack.size();\n    }\n\n    /* Check if the stack is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    public void push(int num) {\n        stack.add(num);\n    }\n\n    /* Pop */\n    public int pop() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.remove(size() - 1);\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.get(size() - 1);\n    }\n\n    /* Convert List to Array and return */\n    public Object[] toArray() {\n        return stack.toArray();\n    }\n}\n
    array_stack.cs
    /* Stack based on array implementation */\nclass ArrayStack {\n    List<int> stack;\n    public ArrayStack() {\n        // Initialize list (dynamic array)\n        stack = [];\n    }\n\n    /* Get the length of the stack */\n    public int Size() {\n        return stack.Count;\n    }\n\n    /* Check if the stack is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Push */\n    public void Push(int num) {\n        stack.Add(num);\n    }\n\n    /* Pop */\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    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stack[Size() - 1];\n    }\n\n    /* Convert List to Array and return */\n    public int[] ToArray() {\n        return [.. stack];\n    }\n}\n
    array_stack.go
    /* Stack based on array implementation */\ntype arrayStack struct {\n    data []int // Data\n}\n\n/* Access top of the stack element */\nfunc newArrayStack() *arrayStack {\n    return &arrayStack{\n        // Set stack length to 0, capacity to 16\n        data: make([]int, 0, 16),\n    }\n}\n\n/* Stack length */\nfunc (s *arrayStack) size() int {\n    return len(s.data)\n}\n\n/* Is stack empty */\nfunc (s *arrayStack) isEmpty() bool {\n    return s.size() == 0\n}\n\n/* Push */\nfunc (s *arrayStack) push(v int) {\n    // Slice will automatically expand\n    s.data = append(s.data, v)\n}\n\n/* Pop */\nfunc (s *arrayStack) pop() any {\n    val := s.peek()\n    s.data = s.data[:len(s.data)-1]\n    return val\n}\n\n/* Get stack top element */\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/* Get Slice for printing */\nfunc (s *arrayStack) toSlice() []int {\n    return s.data\n}\n
    array_stack.swift
    /* Stack based on array implementation */\nclass ArrayStack {\n    private var stack: [Int]\n\n    init() {\n        // Initialize list (dynamic array)\n        stack = []\n    }\n\n    /* Get the length of the stack */\n    func size() -> Int {\n        stack.count\n    }\n\n    /* Check if the stack is empty */\n    func isEmpty() -> Bool {\n        stack.isEmpty\n    }\n\n    /* Push */\n    func push(num: Int) {\n        stack.append(num)\n    }\n\n    /* Pop */\n    @discardableResult\n    func pop() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return stack.removeLast()\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return stack.last!\n    }\n\n    /* Convert List to Array and return */\n    func toArray() -> [Int] {\n        stack\n    }\n}\n
    array_stack.js
    /* Stack based on array implementation */\nclass ArrayStack {\n    #stack;\n    constructor() {\n        this.#stack = [];\n    }\n\n    /* Get the length of the stack */\n    get size() {\n        return this.#stack.length;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty() {\n        return this.#stack.length === 0;\n    }\n\n    /* Push */\n    push(num) {\n        this.#stack.push(num);\n    }\n\n    /* Pop */\n    pop() {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.#stack.pop();\n    }\n\n    /* Return list for printing */\n    top() {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.#stack[this.#stack.length - 1];\n    }\n\n    /* Return Array */\n    toArray() {\n        return this.#stack;\n    }\n}\n
    array_stack.ts
    /* Stack based on array implementation */\nclass ArrayStack {\n    private stack: number[];\n    constructor() {\n        this.stack = [];\n    }\n\n    /* Get the length of the stack */\n    get size(): number {\n        return this.stack.length;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty(): boolean {\n        return this.stack.length === 0;\n    }\n\n    /* Push */\n    push(num: number): void {\n        this.stack.push(num);\n    }\n\n    /* Pop */\n    pop(): number | undefined {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.stack.pop();\n    }\n\n    /* Return list for printing */\n    top(): number | undefined {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.stack[this.stack.length - 1];\n    }\n\n    /* Return Array */\n    toArray() {\n        return this.stack;\n    }\n}\n
    array_stack.dart
    /* Stack based on array implementation */\nclass ArrayStack {\n  late List<int> _stack;\n  ArrayStack() {\n    _stack = [];\n  }\n\n  /* Get the length of the stack */\n  int size() {\n    return _stack.length;\n  }\n\n  /* Check if the stack is empty */\n  bool isEmpty() {\n    return _stack.isEmpty;\n  }\n\n  /* Push */\n  void push(int _num) {\n    _stack.add(_num);\n  }\n\n  /* Pop */\n  int pop() {\n    if (isEmpty()) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stack.removeLast();\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stack.last;\n  }\n\n  /* Convert stack to Array and return */\n  List<int> toArray() => _stack;\n}\n
    array_stack.rs
    /* Stack based on array implementation */\nstruct ArrayStack<T> {\n    stack: Vec<T>,\n}\n\nimpl<T> ArrayStack<T> {\n    /* Access top of the stack element */\n    fn new() -> ArrayStack<T> {\n        ArrayStack::<T> {\n            stack: Vec::<T>::new(),\n        }\n    }\n\n    /* Get the length of the stack */\n    fn size(&self) -> usize {\n        self.stack.len()\n    }\n\n    /* Check if the stack is empty */\n    fn is_empty(&self) -> bool {\n        self.size() == 0\n    }\n\n    /* Push */\n    fn push(&mut self, num: T) {\n        self.stack.push(num);\n    }\n\n    /* Pop */\n    fn pop(&mut self) -> Option<T> {\n        self.stack.pop()\n    }\n\n    /* Return list for printing */\n    fn peek(&self) -> Option<&T> {\n        if self.is_empty() {\n            panic!(\"Stack is empty\")\n        };\n        self.stack.last()\n    }\n\n    /* Return &Vec */\n    fn to_array(&self) -> &Vec<T> {\n        &self.stack\n    }\n}\n
    array_stack.c
    /* Stack based on array implementation */\ntypedef struct {\n    int *data;\n    int size;\n} ArrayStack;\n\n/* Constructor */\nArrayStack *newArrayStack() {\n    ArrayStack *stack = malloc(sizeof(ArrayStack));\n    // Initialize with large capacity to avoid expansion\n    stack->data = malloc(sizeof(int) * MAX_SIZE);\n    stack->size = 0;\n    return stack;\n}\n\n/* Destructor */\nvoid delArrayStack(ArrayStack *stack) {\n    free(stack->data);\n    free(stack);\n}\n\n/* Get the length of the stack */\nint size(ArrayStack *stack) {\n    return stack->size;\n}\n\n/* Check if the stack is empty */\nbool isEmpty(ArrayStack *stack) {\n    return stack->size == 0;\n}\n\n/* Push */\nvoid push(ArrayStack *stack, int num) {\n    if (stack->size == MAX_SIZE) {\n        printf(\"Stack is full\\n\");\n        return;\n    }\n    stack->data[stack->size] = num;\n    stack->size++;\n}\n\n/* Return list for printing */\nint peek(ArrayStack *stack) {\n    if (stack->size == 0) {\n        printf(\"Stack is empty\\n\");\n        return INT_MAX;\n    }\n    return stack->data[stack->size - 1];\n}\n\n/* Pop */\nint pop(ArrayStack *stack) {\n    int val = peek(stack);\n    stack->size--;\n    return val;\n}\n
    array_stack.kt
    /* Stack based on array implementation */\nclass ArrayStack {\n    // Initialize list (dynamic array)\n    private val stack = mutableListOf<Int>()\n\n    /* Get the length of the stack */\n    fun size(): Int {\n        return stack.size\n    }\n\n    /* Check if the stack is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Push */\n    fun push(num: Int) {\n        stack.add(num)\n    }\n\n    /* Pop */\n    fun pop(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack.removeAt(size() - 1)\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack[size() - 1]\n    }\n\n    /* Convert List to Array and return */\n    fun toArray(): Array<Any> {\n        return stack.toTypedArray()\n    }\n}\n
    array_stack.rb
    ### Stack based on array ###\nclass ArrayStack\n  ### Constructor ###\n  def initialize\n    @stack = []\n  end\n\n  ### Get stack length ###\n  def size\n    @stack.length\n  end\n\n  ### Check if stack is empty ###\n  def is_empty?\n    @stack.empty?\n  end\n\n  ### Push ###\n  def push(item)\n    @stack << item\n  end\n\n  ### Pop ###\n  def pop\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @stack.pop\n  end\n\n  ### Access top element ###\n  def peek\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @stack.last\n  end\n\n  ### Return list for printing ###\n  def to_array\n    @stack\n  end\nend\n
    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513-comparison-of-the-two-implementations","level":2,"title":"5.1.3   Comparison of the Two Implementations","text":"

    Supported Operations

    Both implementations support all operations defined by the stack. The array implementation additionally supports random access, but this goes beyond the stack definition and is generally not used.

    Time Efficiency

    In the array-based implementation, both push and pop operations occur in pre-allocated contiguous memory, which has good cache locality and is therefore more efficient. However, if pushing exceeds the array capacity, it triggers an expansion mechanism, causing the time complexity of that particular push operation to become \\(O(n)\\).

    In the linked list-based implementation, list expansion is very flexible, and there is no issue of reduced efficiency due to array expansion. However, the push operation requires initializing a node object and modifying pointers, so it is relatively less efficient. Nevertheless, if the pushed elements are already node objects, the initialization step can be omitted, thereby improving efficiency.

    In summary, when the elements pushed and popped are basic data types such as int or double, we can draw the following conclusions:

    • The array-based stack implementation has reduced efficiency when expansion is triggered, but since expansion is an infrequent operation, the average efficiency is higher.
    • The linked list-based stack implementation can provide more stable efficiency performance.

    Space Efficiency

    When initializing a list, the system allocates an \"initial capacity\" that may exceed the actual need. Additionally, the expansion mechanism typically expands at a specific ratio (e.g., 2x), and the capacity after expansion may also exceed actual needs. Therefore, the array-based stack implementation may cause some space wastage.

    However, since linked list nodes need to store additional pointers, the space occupied by linked list nodes is relatively large.

    In summary, we cannot simply determine which implementation is more memory-efficient and need to analyze the specific situation.

    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514-typical-applications-of-stack","level":2,"title":"5.1.4   Typical Applications of Stack","text":"
    • Back and forward in browsers, undo and redo in software. Every time we open a new webpage, the browser pushes the previous page onto the stack, allowing us to return to the previous page via the back operation. The back operation is essentially performing a pop. To support both back and forward, two stacks are needed to work together.
    • Program memory management. Each time a function is called, the system adds a stack frame to the top of the stack to record the function's context information. During recursion, the downward recursive phase continuously performs push operations, while the upward backtracking phase continuously performs pop operations.
    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   Summary","text":"","path":["Chapter 5. Stacks and Queues","5.4   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • A stack is a data structure that follows the LIFO principle and can be implemented using arrays or linked lists.
    • In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to \\(O(n)\\). In contrast, the linked-list implementation of a stack offers more stable performance.
    • In terms of space efficiency, the array implementation of a stack may lead to some degree of space wastage. However, it should be noted that the memory space occupied by linked list nodes is larger than that of array elements.
    • A queue is a data structure that follows the FIFO principle and can also be implemented using arrays or linked lists. The conclusions regarding time efficiency and space efficiency comparisons for queues are similar to those for stacks mentioned above.
    • A deque is a queue with greater flexibility that allows adding and removing elements at both ends.
    ","path":["Chapter 5. Stacks and Queues","5.4   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Is the browser's forward and backward functionality implemented with a doubly linked list?

    The browser's forward and backward behavior is essentially an application of a \"stack.\" When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. A deque can conveniently support some additional operations, as mentioned in the \"Deque\" section.

    Q: After popping from the stack, do we need to free the memory of the popped node?

    If the popped node will still be needed later, then memory does not need to be freed. If it won't be used afterward, languages like Java and Python have automatic garbage collection, so manual memory deallocation is not required; in C and C++, manual memory deallocation is necessary.

    Q: A deque seems like two stacks joined together. What is its purpose?

    A deque is like a combination of a stack and a queue, or two stacks joined together. It combines the logic of both, so it can support all applications of stacks and queues while offering greater flexibility.

    Q: How are undo and redo specifically implemented?

    Use two stacks: stack A for undo and stack B for redo.

    1. Whenever the user performs an operation, push this operation onto stack A and clear stack B.
    2. When the user performs \"undo,\" pop the most recent operation from stack A and push it onto stack B.
    3. When the user performs \"redo,\" pop the most recent operation from stack B and push it onto stack A.
    ","path":["Chapter 5. Stacks and Queues","5.4   Summary"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"Chapter 7.   Tree","text":"

    Abstract

    Towering trees are full of vitality, with deep roots, lush foliage, and sprawling branches.

    They offer a vivid illustration of divide-and-conquer in data structures.

    ","path":["Chapter 7. Tree","Chapter 7.   Tree"],"tags":[]},{"location":"chapter_tree/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 7.1   Binary Tree
    • 7.2   Binary Tree Traversal
    • 7.3   Array Representation of Binary Trees
    • 7.4   Binary Search Tree
    • 7.5   AVL Tree *
    • 7.6   Summary
    ","path":["Chapter 7. Tree","Chapter 7.   Tree"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   Array Representation of Binary Trees","text":"

    In the linked-list representation, the storage unit of a binary tree is a node TreeNode, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees in this representation.

    So, can we use an array to represent a binary tree? The answer is yes.

    ","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731-representing-perfect-binary-trees","level":2,"title":"7.3.1   Representing Perfect Binary Trees","text":"

    Let's analyze a simple case first. Given a perfect binary tree, we store all nodes in an array according to the order of level-order traversal, where each node corresponds to a unique array index.

    Based on the characteristics of level-order traversal, we can derive a \"mapping formula\" between parent node index and child node indices: If a node's index is \\(i\\), then its left child index is \\(2i + 1\\) and its right child index is \\(2i + 2\\). Figure 7-12 shows the mapping relationships between various node indices.

    Figure 7-12   Array representation of a perfect binary tree

    The mapping formula plays a role similar to the node references (pointers) in linked lists. Given any node in the array, we can access its left (right) child node using the mapping formula.

    ","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732-representing-any-binary-tree","level":2,"title":"7.3.2   Representing Any Binary Tree","text":"

    Perfect binary trees are a special case; in the middle levels of a binary tree, there are typically many None values. Since the level-order traversal sequence does not include these None values, we cannot infer the number and distribution of None values based on this sequence alone. This means multiple binary tree structures can correspond to the same level-order traversal sequence.

    As shown in Figure 7-13, given a non-perfect binary tree, the above method of array representation fails.

    Figure 7-13   Level-order traversal sequence corresponds to multiple binary tree possibilities

    To solve this problem, we can explicitly write out all None values in the level-order traversal sequence. As shown in Figure 7-14, once we do this, the level-order traversal sequence can uniquely represent a binary tree. Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # Array representation of a binary tree\n# Using None to represent empty slots\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n
    /* Array representation of a binary tree */\n// Using the maximum integer value INT_MAX to mark empty slots\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
    /* Array representation of a binary tree */\n// Using the Integer wrapper class allows for using null to mark empty slots\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
    /* Array representation of a binary tree */\n// Using nullable int (int?) allows for using null to mark empty slots\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
    /* Array representation of a binary tree */\n// Using an any type slice, allowing for nil to mark empty slots\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n
    /* Array representation of a binary tree */\n// Using optional Int (Int?) allows for using nil to mark empty slots\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n
    /* Array representation of a binary tree */\n// Using null to represent empty slots\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
    /* Array representation of a binary tree */\n// Using null to represent empty slots\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
    /* Array representation of a binary tree */\n// Using nullable int (int?) allows for using null to mark empty slots\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
    /* Array representation of a binary tree */\n// Using None to mark empty slots\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
    /* Array representation of a binary tree */\n// Using the maximum int value to mark empty slots, therefore, node values must not be 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
    /* Array representation of a binary tree */\n// Using null to represent empty slots\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n
    ### Array representation of a binary tree ###\n# Using nil to represent empty slots\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n

    Figure 7-14   Array representation of an arbitrary binary tree

    It's worth noting that complete binary trees are very well-suited for array representation. Recalling the definition of a complete binary tree, None only appears at the bottom level and towards the right, meaning all None values must appear at the end of the level-order traversal sequence.

    This means that when using an array to represent a complete binary tree, it's possible to omit storing all None values, which is very convenient. Figure 7-15 gives an example.

    Figure 7-15   Array representation of a complete binary tree

    The following code implements a binary tree using an array representation, including the following operations:

    • Given a node, obtain its value, left (right) child node, and parent node.
    • Obtain the preorder, inorder, postorder, and level-order traversal sequences.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py
    class ArrayBinaryTree:\n    \"\"\"Binary tree class represented by array\"\"\"\n\n    def __init__(self, arr: list[int | None]):\n        \"\"\"Constructor\"\"\"\n        self._tree = list(arr)\n\n    def size(self):\n        \"\"\"List capacity\"\"\"\n        return len(self._tree)\n\n    def val(self, i: int) -> int | None:\n        \"\"\"Get value of node at index i\"\"\"\n        # If index is out of bounds, return None, representing empty position\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        \"\"\"Get index of left child node of node at index i\"\"\"\n        return 2 * i + 1\n\n    def right(self, i: int) -> int | None:\n        \"\"\"Get index of right child node of node at index i\"\"\"\n        return 2 * i + 2\n\n    def parent(self, i: int) -> int | None:\n        \"\"\"Get index of parent node of node at index i\"\"\"\n        return (i - 1) // 2\n\n    def level_order(self) -> list[int]:\n        \"\"\"Level-order traversal\"\"\"\n        self.res = []\n        # Traverse array directly\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        \"\"\"Depth-first traversal\"\"\"\n        if self.val(i) is None:\n            return\n        # Preorder traversal\n        if order == \"pre\":\n            self.res.append(self.val(i))\n        self.dfs(self.left(i), order)\n        # Inorder traversal\n        if order == \"in\":\n            self.res.append(self.val(i))\n        self.dfs(self.right(i), order)\n        # Postorder traversal\n        if order == \"post\":\n            self.res.append(self.val(i))\n\n    def pre_order(self) -> list[int]:\n        \"\"\"Preorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"pre\")\n        return self.res\n\n    def in_order(self) -> list[int]:\n        \"\"\"Inorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"in\")\n        return self.res\n\n    def post_order(self) -> list[int]:\n        \"\"\"Postorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"post\")\n        return self.res\n
    array_binary_tree.cpp
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n  public:\n    /* Constructor */\n    ArrayBinaryTree(vector<int> arr) {\n        tree = arr;\n    }\n\n    /* List capacity */\n    int size() {\n        return tree.size();\n    }\n\n    /* Get value of node at index i */\n    int val(int i) {\n        // Return INT_MAX if index out of bounds, representing empty position\n        if (i < 0 || i >= size())\n            return INT_MAX;\n        return tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    int left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    int right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    int parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    vector<int> levelOrder() {\n        vector<int> res;\n        // Traverse array directly\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    /* Preorder traversal */\n    vector<int> preOrder() {\n        vector<int> res;\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    vector<int> inOrder() {\n        vector<int> res;\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\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    /* Depth-first traversal */\n    void dfs(int i, string order, vector<int> &res) {\n        // If empty position, return\n        if (val(i) == INT_MAX)\n            return;\n        // Preorder traversal\n        if (order == \"pre\")\n            res.push_back(val(i));\n        dfs(left(i), order, res);\n        // Inorder traversal\n        if (order == \"in\")\n            res.push_back(val(i));\n        dfs(right(i), order, res);\n        // Postorder traversal\n        if (order == \"post\")\n            res.push_back(val(i));\n    }\n};\n
    array_binary_tree.java
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    private List<Integer> tree;\n\n    /* Constructor */\n    public ArrayBinaryTree(List<Integer> arr) {\n        tree = new ArrayList<>(arr);\n    }\n\n    /* List capacity */\n    public int size() {\n        return tree.size();\n    }\n\n    /* Get value of node at index i */\n    public Integer val(int i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= size())\n            return null;\n        return tree.get(i);\n    }\n\n    /* Get index of left child node of node at index i */\n    public Integer left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    public Integer right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    public Integer parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    public List<Integer> levelOrder() {\n        List<Integer> res = new ArrayList<>();\n        // Traverse array directly\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    /* Depth-first traversal */\n    private void dfs(Integer i, String order, List<Integer> res) {\n        // If empty position, return\n        if (val(i) == null)\n            return;\n        // Preorder traversal\n        if (\"pre\".equals(order))\n            res.add(val(i));\n        dfs(left(i), order, res);\n        // Inorder traversal\n        if (\"in\".equals(order))\n            res.add(val(i));\n        dfs(right(i), order, res);\n        // Postorder traversal\n        if (\"post\".equals(order))\n            res.add(val(i));\n    }\n\n    /* Preorder traversal */\n    public List<Integer> preOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    public List<Integer> inOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\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
    /* Binary tree class represented by array */\nclass ArrayBinaryTree(List<int?> arr) {\n    List<int?> tree = new(arr);\n\n    /* List capacity */\n    public int Size() {\n        return tree.Count;\n    }\n\n    /* Get value of node at index i */\n    public int? Val(int i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= Size())\n            return null;\n        return tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    public int Left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    public int Right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    public int Parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    public List<int> LevelOrder() {\n        List<int> res = [];\n        // Traverse array directly\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    /* Depth-first traversal */\n    void DFS(int i, string order, List<int> res) {\n        // If empty position, return\n        if (!Val(i).HasValue)\n            return;\n        // Preorder traversal\n        if (order == \"pre\")\n            res.Add(Val(i)!.Value);\n        DFS(Left(i), order, res);\n        // Inorder traversal\n        if (order == \"in\")\n            res.Add(Val(i)!.Value);\n        DFS(Right(i), order, res);\n        // Postorder traversal\n        if (order == \"post\")\n            res.Add(Val(i)!.Value);\n    }\n\n    /* Preorder traversal */\n    public List<int> PreOrder() {\n        List<int> res = [];\n        DFS(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    public List<int> InOrder() {\n        List<int> res = [];\n        DFS(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    public List<int> PostOrder() {\n        List<int> res = [];\n        DFS(0, \"post\", res);\n        return res;\n    }\n}\n
    array_binary_tree.go
    /* Binary tree class represented by array */\ntype arrayBinaryTree struct {\n    tree []any\n}\n\n/* Constructor */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n    return &arrayBinaryTree{\n        tree: arr,\n    }\n}\n\n/* List capacity */\nfunc (abt *arrayBinaryTree) size() int {\n    return len(abt.tree)\n}\n\n/* Get value of node at index i */\nfunc (abt *arrayBinaryTree) val(i int) any {\n    // If index out of bounds, return null to represent empty position\n    if i < 0 || i >= abt.size() {\n        return nil\n    }\n    return abt.tree[i]\n}\n\n/* Get index of left child node of node at index i */\nfunc (abt *arrayBinaryTree) left(i int) int {\n    return 2*i + 1\n}\n\n/* Get index of right child node of node at index i */\nfunc (abt *arrayBinaryTree) right(i int) int {\n    return 2*i + 2\n}\n\n/* Get index of parent node of node at index i */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n    return (i - 1) / 2\n}\n\n/* Level-order traversal */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n    var res []any\n    // Traverse array directly\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/* Depth-first traversal */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n    // If empty position, return\n    if abt.val(i) == nil {\n        return\n    }\n    // Preorder traversal\n    if order == \"pre\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.left(i), order, res)\n    // Inorder traversal\n    if order == \"in\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.right(i), order, res)\n    // Postorder traversal\n    if order == \"post\" {\n        *res = append(*res, abt.val(i))\n    }\n}\n\n/* Preorder traversal */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n    var res []any\n    abt.dfs(0, \"pre\", &res)\n    return res\n}\n\n/* Inorder traversal */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n    var res []any\n    abt.dfs(0, \"in\", &res)\n    return res\n}\n\n/* Postorder traversal */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n    var res []any\n    abt.dfs(0, \"post\", &res)\n    return res\n}\n
    array_binary_tree.swift
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    private var tree: [Int?]\n\n    /* Constructor */\n    init(arr: [Int?]) {\n        tree = arr\n    }\n\n    /* List capacity */\n    func size() -> Int {\n        tree.count\n    }\n\n    /* Get value of node at index i */\n    func val(i: Int) -> Int? {\n        // If index out of bounds, return null to represent empty position\n        if i < 0 || i >= size() {\n            return nil\n        }\n        return tree[i]\n    }\n\n    /* Get index of left child node of node at index i */\n    func left(i: Int) -> Int {\n        2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    func right(i: Int) -> Int {\n        2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    func parent(i: Int) -> Int {\n        (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    func levelOrder() -> [Int] {\n        var res: [Int] = []\n        // Traverse array directly\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    /* Depth-first traversal */\n    private func dfs(i: Int, order: String, res: inout [Int]) {\n        // If empty position, return\n        guard let val = val(i: i) else {\n            return\n        }\n        // Preorder traversal\n        if order == \"pre\" {\n            res.append(val)\n        }\n        dfs(i: left(i: i), order: order, res: &res)\n        // Inorder traversal\n        if order == \"in\" {\n            res.append(val)\n        }\n        dfs(i: right(i: i), order: order, res: &res)\n        // Postorder traversal\n        if order == \"post\" {\n            res.append(val)\n        }\n    }\n\n    /* Preorder traversal */\n    func preOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"pre\", res: &res)\n        return res\n    }\n\n    /* Inorder traversal */\n    func inOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"in\", res: &res)\n        return res\n    }\n\n    /* Postorder traversal */\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
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    #tree;\n\n    /* Constructor */\n    constructor(arr) {\n        this.#tree = arr;\n    }\n\n    /* List capacity */\n    size() {\n        return this.#tree.length;\n    }\n\n    /* Get value of node at index i */\n    val(i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    left(i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    right(i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    parent(i) {\n        return Math.floor((i - 1) / 2); // Floor division\n    }\n\n    /* Level-order traversal */\n    levelOrder() {\n        let res = [];\n        // Traverse array directly\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    /* Depth-first traversal */\n    #dfs(i, order, res) {\n        // If empty position, return\n        if (this.val(i) === null) return;\n        // Preorder traversal\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Inorder traversal\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Postorder traversal\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Preorder traversal */\n    preOrder() {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    inOrder() {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    postOrder() {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
    array_binary_tree.ts
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    #tree: (number | null)[];\n\n    /* Constructor */\n    constructor(arr: (number | null)[]) {\n        this.#tree = arr;\n    }\n\n    /* List capacity */\n    size(): number {\n        return this.#tree.length;\n    }\n\n    /* Get value of node at index i */\n    val(i: number): number | null {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    left(i: number): number {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    right(i: number): number {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    parent(i: number): number {\n        return Math.floor((i - 1) / 2); // Floor division\n    }\n\n    /* Level-order traversal */\n    levelOrder(): number[] {\n        let res = [];\n        // Traverse array directly\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    /* Depth-first traversal */\n    #dfs(i: number, order: Order, res: (number | null)[]): void {\n        // If empty position, return\n        if (this.val(i) === null) return;\n        // Preorder traversal\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Inorder traversal\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Postorder traversal\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Preorder traversal */\n    preOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    inOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    postOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
    array_binary_tree.dart
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n  late List<int?> _tree;\n\n  /* Constructor */\n  ArrayBinaryTree(this._tree);\n\n  /* List capacity */\n  int size() {\n    return _tree.length;\n  }\n\n  /* Get value of node at index i */\n  int? val(int i) {\n    // If index out of bounds, return null to represent empty position\n    if (i < 0 || i >= size()) {\n      return null;\n    }\n    return _tree[i];\n  }\n\n  /* Get index of left child node of node at index i */\n  int? left(int i) {\n    return 2 * i + 1;\n  }\n\n  /* Get index of right child node of node at index i */\n  int? right(int i) {\n    return 2 * i + 2;\n  }\n\n  /* Get index of parent node of node at index i */\n  int? parent(int i) {\n    return (i - 1) ~/ 2;\n  }\n\n  /* Level-order traversal */\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  /* Depth-first traversal */\n  void dfs(int i, String order, List<int?> res) {\n    // If empty position, return\n    if (val(i) == null) {\n      return;\n    }\n    // Preorder traversal\n    if (order == 'pre') {\n      res.add(val(i));\n    }\n    dfs(left(i)!, order, res);\n    // Inorder traversal\n    if (order == 'in') {\n      res.add(val(i));\n    }\n    dfs(right(i)!, order, res);\n    // Postorder traversal\n    if (order == 'post') {\n      res.add(val(i));\n    }\n  }\n\n  /* Preorder traversal */\n  List<int?> preOrder() {\n    List<int?> res = [];\n    dfs(0, 'pre', res);\n    return res;\n  }\n\n  /* Inorder traversal */\n  List<int?> inOrder() {\n    List<int?> res = [];\n    dfs(0, 'in', res);\n    return res;\n  }\n\n  /* Postorder traversal */\n  List<int?> postOrder() {\n    List<int?> res = [];\n    dfs(0, 'post', res);\n    return res;\n  }\n}\n
    array_binary_tree.rs
    /* Binary tree class represented by array */\nstruct ArrayBinaryTree {\n    tree: Vec<Option<i32>>,\n}\n\nimpl ArrayBinaryTree {\n    /* Constructor */\n    fn new(arr: Vec<Option<i32>>) -> Self {\n        Self { tree: arr }\n    }\n\n    /* List capacity */\n    fn size(&self) -> i32 {\n        self.tree.len() as i32\n    }\n\n    /* Get value of node at index i */\n    fn val(&self, i: i32) -> Option<i32> {\n        // If index is out of bounds, return None, representing empty position\n        if i < 0 || i >= self.size() {\n            None\n        } else {\n            self.tree[i as usize]\n        }\n    }\n\n    /* Get index of left child node of node at index i */\n    fn left(&self, i: i32) -> i32 {\n        2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    fn right(&self, i: i32) -> i32 {\n        2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    fn parent(&self, i: i32) -> i32 {\n        (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    fn level_order(&self) -> Vec<i32> {\n        self.tree.iter().filter_map(|&x| x).collect()\n    }\n\n    /* Depth-first traversal */\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        // Preorder traversal\n        if order == \"pre\" {\n            res.push(val);\n        }\n        self.dfs(self.left(i), order, res);\n        // Inorder traversal\n        if order == \"in\" {\n            res.push(val);\n        }\n        self.dfs(self.right(i), order, res);\n        // Postorder traversal\n        if order == \"post\" {\n            res.push(val);\n        }\n    }\n\n    /* Preorder traversal */\n    fn pre_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"pre\", &mut res);\n        res\n    }\n\n    /* Inorder traversal */\n    fn in_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"in\", &mut res);\n        res\n    }\n\n    /* Postorder traversal */\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
    /* Binary tree structure in array representation */\ntypedef struct {\n    int *tree;\n    int size;\n} ArrayBinaryTree;\n\n/* Constructor */\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/* Destructor */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n    free(abt->tree);\n    free(abt);\n}\n\n/* List capacity */\nint size(ArrayBinaryTree *abt) {\n    return abt->size;\n}\n\n/* Get value of node at index i */\nint val(ArrayBinaryTree *abt, int i) {\n    // Return INT_MAX if index out of bounds, representing empty position\n    if (i < 0 || i >= size(abt))\n        return INT_MAX;\n    return abt->tree[i];\n}\n\n/* Level-order traversal */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    // Traverse array directly\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/* Depth-first traversal */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n    // If empty position, return\n    if (val(abt, i) == INT_MAX)\n        return;\n    // Preorder traversal\n    if (strcmp(order, \"pre\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, left(i), order, res, index);\n    // Inorder traversal\n    if (strcmp(order, \"in\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, right(i), order, res, index);\n    // Postorder traversal\n    if (strcmp(order, \"post\") == 0)\n        res[(*index)++] = val(abt, i);\n}\n\n/* Preorder traversal */\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/* Inorder traversal */\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/* Postorder traversal */\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
    /* Binary tree class represented by array */\nclass ArrayBinaryTree(val tree: MutableList<Int?>) {\n    /* List capacity */\n    fun size(): Int {\n        return tree.size\n    }\n\n    /* Get value of node at index i */\n    fun _val(i: Int): Int? {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= size()) return null\n        return tree[i]\n    }\n\n    /* Get index of left child node of node at index i */\n    fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    fun parent(i: Int): Int {\n        return (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    fun levelOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        // Traverse array directly\n        for (i in 0..<size()) {\n            if (_val(i) != null)\n                res.add(_val(i))\n        }\n        return res\n    }\n\n    /* Depth-first traversal */\n    fun dfs(i: Int, order: String, res: MutableList<Int?>) {\n        // If empty position, return\n        if (_val(i) == null)\n            return\n        // Preorder traversal\n        if (\"pre\" == order)\n            res.add(_val(i))\n        dfs(left(i), order, res)\n        // Inorder traversal\n        if (\"in\" == order)\n            res.add(_val(i))\n        dfs(right(i), order, res)\n        // Postorder traversal\n        if (\"post\" == order)\n            res.add(_val(i))\n    }\n\n    /* Preorder traversal */\n    fun preOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"pre\", res)\n        return res\n    }\n\n    /* Inorder traversal */\n    fun inOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"in\", res)\n        return res\n    }\n\n    /* Postorder traversal */\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
    ### Array representation of binary tree class ###\nclass ArrayBinaryTree\n  ### Constructor ###\n  def initialize(arr)\n    @tree = arr.to_a\n  end\n\n  ### List capacity ###\n  def size\n    @tree.length\n  end\n\n  ### Get value of node at index i ###\n  def val(i)\n    # Return nil if index out of bounds, representing empty position\n    return if i < 0 || i >= size\n\n    @tree[i]\n  end\n\n  ### Get left child index of node at index i ###\n  def left(i)\n    2 * i + 1\n  end\n\n  ### Get right child index of node at index i ###\n  def right(i)\n    2 * i + 2\n  end\n\n  ### Get parent node index of node at index i ###\n  def parent(i)\n    (i - 1) / 2\n  end\n\n  ### Level-order traversal ###\n  def level_order\n    @res = []\n\n    # Traverse array directly\n    for i in 0...size\n      @res << val(i) unless val(i).nil?\n    end\n\n    @res\n  end\n\n  ### Depth-first traversal ###\n  def dfs(i, order)\n    return if val(i).nil?\n    # Preorder traversal\n    @res << val(i) if order == :pre\n    dfs(left(i), order)\n    # Inorder traversal\n    @res << val(i) if order == :in\n    dfs(right(i), order)\n    # Postorder traversal\n    @res << val(i) if order == :post\n  end\n\n  ### Pre-order traversal ###\n  def pre_order\n    @res = []\n    dfs(0, :pre)\n    @res\n  end\n\n  ### In-order traversal ###\n  def in_order\n    @res = []\n    dfs(0, :in)\n    @res\n  end\n\n  ### Post-order traversal ###\n  def post_order\n    @res = []\n    dfs(0, :post)\n    @res\n  end\nend\n
    ","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733-advantages-and-limitations","level":2,"title":"7.3.3   Advantages and Limitations","text":"

    The array representation of binary trees has the following advantages:

    • Arrays are stored in contiguous memory space, which is cache-friendly, allowing faster access and traversal.
    • It does not require storing pointers, which saves space.
    • It allows random access to nodes.

    However, the array representation also has some limitations:

    • Array storage requires contiguous memory space, so it is not suitable for storing trees with a large amount of data.
    • Adding or removing nodes requires array insertion and deletion operations, which have lower efficiency.
    • When there are many None values in the binary tree, the proportion of node data contained in the array is low, leading to lower space utilization.
    ","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   AVL Tree *","text":"

    In the \"Binary Search Tree\" section, we mentioned that after multiple insertion and removal operations, a binary search tree may degenerate into a linked list. In this case, the time complexity of all operations degrades from \\(O(\\log n)\\) to \\(O(n)\\).

    As shown in Figure 7-24, after two node removal operations, this binary search tree will degrade into a linked list.

    Figure 7-24   Degradation of an AVL tree after removing nodes

    For example, in the perfect binary tree shown in Figure 7-25, after inserting two nodes, the tree will lean heavily to the left, and the time complexity of search operations will also degrade.

    Figure 7-25   Degradation of an AVL tree after inserting nodes

    In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL tree in their paper \"An algorithm for the organization of information\". The paper describes a series of operations that prevent an AVL tree from degenerating as nodes are inserted and removed, thereby keeping the time complexity of various operations at \\(O(\\log n)\\). In other words, in scenarios that require frequent insertion, deletion, lookup, and update operations, AVL trees can maintain consistently efficient performance and therefore have strong practical value.

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-common-terminology-in-avl-trees","level":2,"title":"7.5.1   Common Terminology in AVL Trees","text":"

    An AVL tree is both a binary search tree and a balanced binary tree, simultaneously satisfying all the properties of these two types of binary trees, hence it is a balanced binary search tree.

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-node-height","level":3,"title":"1.   Node Height","text":"

    Since the operations related to AVL trees require obtaining node heights, we need to add a height variable to the node class:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class TreeNode:\n    \"\"\"AVL tree node\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                 # Node value\n        self.height: int = 0                # Node height\n        self.left: TreeNode | None = None   # Left child reference\n        self.right: TreeNode | None = None  # Right child reference\n
    /* AVL tree node */\nstruct TreeNode {\n    int val{};          // Node value\n    int height = 0;     // Node height\n    TreeNode *left{};   // Left child\n    TreeNode *right{};  // Right child\n    TreeNode() = default;\n    explicit TreeNode(int x) : val(x){}\n};\n
    /* AVL tree node */\nclass TreeNode {\n    public int val;        // Node value\n    public int height;     // Node height\n    public TreeNode left;  // Left child\n    public TreeNode right; // Right child\n    public TreeNode(int x) { val = x; }\n}\n
    /* AVL tree node */\nclass TreeNode(int? x) {\n    public int? val = x;    // Node value\n    public int height;      // Node height\n    public TreeNode? left;  // Left child reference\n    public TreeNode? right; // Right child reference\n}\n
    /* AVL tree node */\ntype TreeNode struct {\n    Val    int       // Node value\n    Height int       // Node height\n    Left   *TreeNode // Left child reference\n    Right  *TreeNode // Right child reference\n}\n
    /* AVL tree node */\nclass TreeNode {\n    var val: Int // Node value\n    var height: Int // Node height\n    var left: TreeNode? // Left child\n    var right: TreeNode? // Right child\n\n    init(x: Int) {\n        val = x\n        height = 0\n    }\n}\n
    /* AVL tree node */\nclass TreeNode {\n    val; // Node value\n    height; // Node height\n    left; // Left child pointer\n    right; // Right child pointer\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 tree node */\nclass TreeNode {\n    val: number;            // Node value\n    height: number;         // Node height\n    left: TreeNode | null;  // Left child pointer\n    right: TreeNode | null; // Right child pointer\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 tree node */\nclass TreeNode {\n  int val;         // Node value\n  int height;      // Node height\n  TreeNode? left;  // Left child\n  TreeNode? right; // Right child\n  TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* AVL tree node */\nstruct TreeNode {\n    val: i32,                               // Node value\n    height: i32,                            // Node height\n    left: Option<Rc<RefCell<TreeNode>>>,    // Left child\n    right: Option<Rc<RefCell<TreeNode>>>,   // Right child\n}\n\nimpl TreeNode {\n    /* Constructor */\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 tree node */\ntypedef struct TreeNode {\n    int val;\n    int height;\n    struct TreeNode *left;\n    struct TreeNode *right;\n} TreeNode;\n\n/* Constructor */\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 tree node */\nclass TreeNode(val _val: Int) {  // Node value\n    val height: Int = 0          // Node height\n    val left: TreeNode? = null   // Left child\n    val right: TreeNode? = null  // Right child\n}\n
    ### AVL tree node class ###\nclass TreeNode\n  attr_accessor :val    # Node value\n  attr_accessor :height # Node height\n  attr_accessor :left   # Left child reference\n  attr_accessor :right  # Right child reference\n\n  def initialize(val)\n    @val = val\n    @height = 0\n  end\nend\n

    The \"node height\" refers to the distance from that node to its farthest leaf node, i.e., the number of edges on the path. It is important to note that the height of a leaf node is \\(0\\), and the height of a null node is \\(-1\\). We will create two utility functions for getting and updating the height of a node:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def height(self, node: TreeNode | None) -> int:\n    \"\"\"Get node height\"\"\"\n    # Empty node height is -1, leaf node height is 0\n    if node is not None:\n        return node.height\n    return -1\n\ndef update_height(self, node: TreeNode | None):\n    \"\"\"Update node height\"\"\"\n    # Node height equals the height of the tallest subtree + 1\n    node.height = max([self.height(node.left), self.height(node.right)]) + 1\n
    avl_tree.cpp
    /* Get node height */\nint height(TreeNode *node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == nullptr ? -1 : node->height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode *node) {\n    // Node height equals the height of the tallest subtree + 1\n    node->height = max(height(node->left), height(node->right)) + 1;\n}\n
    avl_tree.java
    /* Get node height */\nint height(TreeNode node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n
    avl_tree.cs
    /* Get node height */\nint Height(TreeNode? node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid UpdateHeight(TreeNode node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n
    avl_tree.go
    /* Get node height */\nfunc (t *aVLTree) height(node *TreeNode) int {\n    // Empty node height is -1, leaf node height is 0\n    if node != nil {\n        return node.Height\n    }\n    return -1\n}\n\n/* Update node height */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n    lh := t.height(node.Left)\n    rh := t.height(node.Right)\n    // Node height equals the height of the tallest subtree + 1\n    if lh > rh {\n        node.Height = lh + 1\n    } else {\n        node.Height = rh + 1\n    }\n}\n
    avl_tree.swift
    /* Get node height */\nfunc height(node: TreeNode?) -> Int {\n    // Empty node height is -1, leaf node height is 0\n    node?.height ?? -1\n}\n\n/* Update node height */\nfunc updateHeight(node: TreeNode?) {\n    // Node height equals the height of the tallest subtree + 1\n    node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n
    avl_tree.js
    /* Get node height */\nheight(node) {\n    // Empty node height is -1, leaf node height is 0\n    return node === null ? -1 : node.height;\n}\n\n/* Update node height */\n#updateHeight(node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
    avl_tree.ts
    /* Get node height */\nheight(node: TreeNode): number {\n    // Empty node height is -1, leaf node height is 0\n    return node === null ? -1 : node.height;\n}\n\n/* Update node height */\nupdateHeight(node: TreeNode): void {\n    // Node height equals the height of the tallest subtree + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
    avl_tree.dart
    /* Get node height */\nint height(TreeNode? node) {\n  // Empty node height is -1, leaf node height is 0\n  return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode? node) {\n  // Node height equals the height of the tallest subtree + 1\n  node!.height = max(height(node.left), height(node.right)) + 1;\n}\n
    avl_tree.rs
    /* Get node height */\nfn height(node: OptionTreeNodeRc) -> i32 {\n    // Empty node height is -1, leaf node height is 0\n    match node {\n        Some(node) => node.borrow().height,\n        None => -1,\n    }\n}\n\n/* Update node height */\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        // Node height equals the height of the tallest subtree + 1\n        node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n    }\n}\n
    avl_tree.c
    /* Get node height */\nint height(TreeNode *node) {\n    // Empty node height is -1, leaf node height is 0\n    if (node != NULL) {\n        return node->height;\n    }\n    return -1;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode *node) {\n    int lh = height(node->left);\n    int rh = height(node->right);\n    // Node height equals the height of the tallest subtree + 1\n    if (lh > rh) {\n        node->height = lh + 1;\n    } else {\n        node->height = rh + 1;\n    }\n}\n
    avl_tree.kt
    /* Get node height */\nfun height(node: TreeNode?): Int {\n    // Empty node height is -1, leaf node height is 0\n    return node?.height ?: -1\n}\n\n/* Update node height */\nfun updateHeight(node: TreeNode?) {\n    // Node height equals the height of the tallest subtree + 1\n    node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n
    avl_tree.rb
    ### Get node height ###\ndef height(node)\n  # Empty node height is -1, leaf node height is 0\n  return node.height unless node.nil?\n\n  -1\nend\n\n### Update node height ###\ndef update_height(node)\n  # Node height equals the height of the tallest subtree + 1\n  node.height = [height(node.left), height(node.right)].max + 1\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-node-balance-factor","level":3,"title":"2.   Node Balance Factor","text":"

    The balance factor of a node is defined as the height of the node's left subtree minus the height of its right subtree, and the balance factor of a null node is defined as \\(0\\). We also encapsulate the function to obtain the node's balance factor for convenient subsequent use:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def balance_factor(self, node: TreeNode | None) -> int:\n    \"\"\"Get balance factor\"\"\"\n    # Empty node balance factor is 0\n    if node is None:\n        return 0\n    # Node balance factor = left subtree height - right subtree height\n    return self.height(node.left) - self.height(node.right)\n
    avl_tree.cpp
    /* Get balance factor */\nint balanceFactor(TreeNode *node) {\n    // Empty node balance factor is 0\n    if (node == nullptr)\n        return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return height(node->left) - height(node->right);\n}\n
    avl_tree.java
    /* Get balance factor */\nint balanceFactor(TreeNode node) {\n    // Empty node balance factor is 0\n    if (node == null)\n        return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return height(node.left) - height(node.right);\n}\n
    avl_tree.cs
    /* Get balance factor */\nint BalanceFactor(TreeNode? node) {\n    // Empty node balance factor is 0\n    if (node == null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return Height(node.left) - Height(node.right);\n}\n
    avl_tree.go
    /* Get balance factor */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n    // Empty node balance factor is 0\n    if node == nil {\n        return 0\n    }\n    // Node balance factor = left subtree height - right subtree height\n    return t.height(node.Left) - t.height(node.Right)\n}\n
    avl_tree.swift
    /* Get balance factor */\nfunc balanceFactor(node: TreeNode?) -> Int {\n    // Empty node balance factor is 0\n    guard let node = node else { return 0 }\n    // Node balance factor = left subtree height - right subtree height\n    return height(node: node.left) - height(node: node.right)\n}\n
    avl_tree.js
    /* Get balance factor */\nbalanceFactor(node) {\n    // Empty node balance factor is 0\n    if (node === null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return this.height(node.left) - this.height(node.right);\n}\n
    avl_tree.ts
    /* Get balance factor */\nbalanceFactor(node: TreeNode): number {\n    // Empty node balance factor is 0\n    if (node === null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return this.height(node.left) - this.height(node.right);\n}\n
    avl_tree.dart
    /* Get balance factor */\nint balanceFactor(TreeNode? node) {\n  // Empty node balance factor is 0\n  if (node == null) return 0;\n  // Node balance factor = left subtree height - right subtree height\n  return height(node.left) - height(node.right);\n}\n
    avl_tree.rs
    /* Get balance factor */\nfn balance_factor(node: OptionTreeNodeRc) -> i32 {\n    match node {\n        // Empty node balance factor is 0\n        None => 0,\n        // Node balance factor = left subtree height - right subtree height\n        Some(node) => {\n            Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n        }\n    }\n}\n
    avl_tree.c
    /* Get balance factor */\nint balanceFactor(TreeNode *node) {\n    // Empty node balance factor is 0\n    if (node == NULL) {\n        return 0;\n    }\n    // Node balance factor = left subtree height - right subtree height\n    return height(node->left) - height(node->right);\n}\n
    avl_tree.kt
    /* Get balance factor */\nfun balanceFactor(node: TreeNode?): Int {\n    // Empty node balance factor is 0\n    if (node == null) return 0\n    // Node balance factor = left subtree height - right subtree height\n    return height(node.left) - height(node.right)\n}\n
    avl_tree.rb
    ### Get balance factor ###\ndef balance_factor(node)\n  # Empty node balance factor is 0\n  return 0 if node.nil?\n\n  # Node balance factor = left subtree height - right subtree height\n  height(node.left) - height(node.right)\nend\n

    Tip

    Let the balance factor be \\(f\\), then the balance factor of any node in an AVL tree satisfies \\(-1 \\le f \\le 1\\).

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-rotations-in-avl-trees","level":2,"title":"7.5.2   Rotations in AVL Trees","text":"

    The characteristic of AVL trees lies in the \"rotation\" operation, which can restore balance to unbalanced nodes without affecting the inorder traversal sequence of the binary tree. In other words, rotation operations can both maintain the property of a \"binary search tree\" and make the tree return to a \"balanced binary tree\".

    We call nodes with a balance factor absolute value \\(> 1\\) \"unbalanced nodes\". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, right rotation then left rotation, and left rotation then right rotation. Below we describe these rotation operations in detail.

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-right-rotation","level":3,"title":"1.   Right Rotation","text":"

    As shown in Figure 7-26, the value below the node is the balance factor. From bottom to top, the first unbalanced node in the binary tree is \"node 3\". We focus on the subtree with this unbalanced node as the root, denoting the node as node and its left child as child, and perform a \"right rotation\" operation. After the right rotation is completed, the subtree regains balance and still maintains the properties of a binary search tree.

    <1><2><3><4>

    Figure 7-26   Steps of right rotation

    As shown in Figure 7-27, when the child node has a right child (denoted as grand_child), a step needs to be added in the right rotation: set grand_child as the left child of node.

    Figure 7-27   Right rotation with grand_child

    \"Right rotation\" is a figurative term; in practice, it is achieved by modifying node pointers, as shown in the following code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Right rotation operation\"\"\"\n    child = node.left\n    grand_child = child.right\n    # Using child as pivot, rotate node to the right\n    child.right = node\n    node.left = grand_child\n    # Update node height\n    self.update_height(node)\n    self.update_height(child)\n    # Return root node of subtree after rotation\n    return child\n
    avl_tree.cpp
    /* Right rotation operation */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child = node->left;\n    TreeNode *grandChild = child->right;\n    // Using child as pivot, rotate node to the right\n    child->right = node;\n    node->left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.java
    /* Right rotation operation */\nTreeNode rightRotate(TreeNode node) {\n    TreeNode child = node.left;\n    TreeNode grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.cs
    /* Right rotation operation */\nTreeNode? RightRotate(TreeNode? node) {\n    TreeNode? child = node?.left;\n    TreeNode? grandChild = child?.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.go
    /* Right rotation operation */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n    child := node.Left\n    grandChild := child.Right\n    // Using child as pivot, rotate node to the right\n    child.Right = node\n    node.Left = grandChild\n    // Update node height\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.swift
    /* Right rotation operation */\nfunc rightRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.left\n    let grandChild = child?.right\n    // Using child as pivot, rotate node to the right\n    child?.right = node\n    node?.left = grandChild\n    // Update node height\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.js
    /* Right rotation operation */\n#rightRotate(node) {\n    const child = node.left;\n    const grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.ts
    /* Right rotation operation */\nrightRotate(node: TreeNode): TreeNode {\n    const child = node.left;\n    const grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.dart
    /* Right rotation operation */\nTreeNode? rightRotate(TreeNode? node) {\n  TreeNode? child = node!.left;\n  TreeNode? grandChild = child!.right;\n  // Using child as pivot, rotate node to the right\n  child.right = node;\n  node.left = grandChild;\n  // Update node height\n  updateHeight(node);\n  updateHeight(child);\n  // Return root node of subtree after rotation\n  return child;\n}\n
    avl_tree.rs
    /* Right rotation operation */\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            // Using child as pivot, rotate node to the right\n            child.borrow_mut().right = Some(node.clone());\n            node.borrow_mut().left = grand_child;\n            // Update node height\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Return root node of subtree after rotation\n            Some(child)\n        }\n        None => None,\n    }\n}\n
    avl_tree.c
    /* Right rotation operation */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->left;\n    grandChild = child->right;\n    // Using child as pivot, rotate node to the right\n    child->right = node;\n    node->left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.kt
    /* Right rotation operation */\nfun rightRotate(node: TreeNode?): TreeNode {\n    val child = node!!.left\n    val grandChild = child!!.right\n    // Using child as pivot, rotate node to the right\n    child.right = node\n    node.left = grandChild\n    // Update node height\n    updateHeight(node)\n    updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.rb
    ### Right rotation ###\ndef right_rotate(node)\n  child = node.left\n  grand_child = child.right\n  # Using child as pivot, rotate node to the right\n  child.right = node\n  node.left = grand_child\n  # Update node height\n  update_height(node)\n  update_height(child)\n  # Return root node of subtree after rotation\n  child\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-left-rotation","level":3,"title":"2.   Left Rotation","text":"

    Correspondingly, if considering the \"mirror\" of the above unbalanced binary tree, the \"left rotation\" operation shown in Figure 7-28 needs to be performed.

    Figure 7-28   Left rotation operation

    Similarly, as shown in Figure 7-29, when the child node has a left child (denoted as grand_child), a step needs to be added in the left rotation: set grand_child as the right child of node.

    Figure 7-29   Left rotation with grand_child

    It can be observed that right rotation and left rotation operations are mirror symmetric in logic, and the two imbalance cases they solve are also symmetric. Based on symmetry, we only need to replace all left in the right rotation implementation code with right, and all right with left, to obtain the left rotation implementation code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Left rotation operation\"\"\"\n    child = node.right\n    grand_child = child.left\n    # Using child as pivot, rotate node to the left\n    child.left = node\n    node.right = grand_child\n    # Update node height\n    self.update_height(node)\n    self.update_height(child)\n    # Return root node of subtree after rotation\n    return child\n
    avl_tree.cpp
    /* Left rotation operation */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child = node->right;\n    TreeNode *grandChild = child->left;\n    // Using child as pivot, rotate node to the left\n    child->left = node;\n    node->right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.java
    /* Left rotation operation */\nTreeNode leftRotate(TreeNode node) {\n    TreeNode child = node.right;\n    TreeNode grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.cs
    /* Left rotation operation */\nTreeNode? LeftRotate(TreeNode? node) {\n    TreeNode? child = node?.right;\n    TreeNode? grandChild = child?.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.go
    /* Left rotation operation */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n    child := node.Right\n    grandChild := child.Left\n    // Using child as pivot, rotate node to the left\n    child.Left = node\n    node.Right = grandChild\n    // Update node height\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.swift
    /* Left rotation operation */\nfunc leftRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.right\n    let grandChild = child?.left\n    // Using child as pivot, rotate node to the left\n    child?.left = node\n    node?.right = grandChild\n    // Update node height\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.js
    /* Left rotation operation */\n#leftRotate(node) {\n    const child = node.right;\n    const grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.ts
    /* Left rotation operation */\nleftRotate(node: TreeNode): TreeNode {\n    const child = node.right;\n    const grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.dart
    /* Left rotation operation */\nTreeNode? leftRotate(TreeNode? node) {\n  TreeNode? child = node!.right;\n  TreeNode? grandChild = child!.left;\n  // Using child as pivot, rotate node to the left\n  child.left = node;\n  node.right = grandChild;\n  // Update node height\n  updateHeight(node);\n  updateHeight(child);\n  // Return root node of subtree after rotation\n  return child;\n}\n
    avl_tree.rs
    /* Left rotation operation */\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            // Using child as pivot, rotate node to the left\n            child.borrow_mut().left = Some(node.clone());\n            node.borrow_mut().right = grand_child;\n            // Update node height\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Return root node of subtree after rotation\n            Some(child)\n        }\n        None => None,\n    }\n}\n
    avl_tree.c
    /* Left rotation operation */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->right;\n    grandChild = child->left;\n    // Using child as pivot, rotate node to the left\n    child->left = node;\n    node->right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.kt
    /* Left rotation operation */\nfun leftRotate(node: TreeNode?): TreeNode {\n    val child = node!!.right\n    val grandChild = child!!.left\n    // Using child as pivot, rotate node to the left\n    child.left = node\n    node.right = grandChild\n    // Update node height\n    updateHeight(node)\n    updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.rb
    ### Left rotation ###\ndef left_rotate(node)\n  child = node.right\n  grand_child = child.left\n  # Using child as pivot, rotate node to the left\n  child.left = node\n  node.right = grand_child\n  # Update node height\n  update_height(node)\n  update_height(child)\n  # Return root node of subtree after rotation\n  child\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3-left-rotation-then-right-rotation","level":3,"title":"3.   Left Rotation Then Right Rotation","text":"

    For the unbalanced node 3 in Figure 7-30, using either left rotation or right rotation alone cannot restore the subtree to balance. In this case, a \"left rotation\" needs to be performed on child first, followed by a \"right rotation\" on node.

    Figure 7-30   Left-right rotation

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4-right-rotation-then-left-rotation","level":3,"title":"4.   Right Rotation Then Left Rotation","text":"

    As shown in Figure 7-31, for the mirror case of the above unbalanced binary tree, a \"right rotation\" needs to be performed on child first, then a \"left rotation\" on node.

    Figure 7-31   Right-left rotation

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5-choice-of-rotation","level":3,"title":"5.   Choice of Rotation","text":"

    The four imbalances shown in Figure 7-32 correspond one-to-one with the above cases, requiring right rotation, left rotation then right rotation, right rotation then left rotation, and left rotation operations respectively.

    Figure 7-32   The four rotation cases of AVL tree

    As shown in Table 7-3, we determine which case the unbalanced node belongs to by judging the signs of the balance factor of the unbalanced node and the balance factor of its taller-side child node.

    Table 7-3   Conditions for Choosing Among the Four Rotation Cases

    Balance factor of the unbalanced node Balance factor of the child node Rotation method to apply \\(> 1\\) (left-leaning tree) \\(\\geq 0\\) Right rotation \\(> 1\\) (left-leaning tree) \\(<0\\) Left rotation then right rotation \\(< -1\\) (right-leaning tree) \\(\\leq 0\\) Left rotation \\(< -1\\) (right-leaning tree) \\(>0\\) Right rotation then left rotation

    For ease of use, we encapsulate the rotation operations into a function. With this function, we can perform rotations for various imbalance situations, restoring balance to unbalanced nodes. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Perform rotation operation to restore balance to this subtree\"\"\"\n    # Get balance factor of node\n    balance_factor = self.balance_factor(node)\n    # Left-leaning tree\n    if balance_factor > 1:\n        if self.balance_factor(node.left) >= 0:\n            # Right rotation\n            return self.right_rotate(node)\n        else:\n            # First left rotation then right rotation\n            node.left = self.left_rotate(node.left)\n            return self.right_rotate(node)\n    # Right-leaning tree\n    elif balance_factor < -1:\n        if self.balance_factor(node.right) <= 0:\n            # Left rotation\n            return self.left_rotate(node)\n        else:\n            # First right rotation then left rotation\n            node.right = self.right_rotate(node.right)\n            return self.left_rotate(node)\n    # Balanced tree, no rotation needed, return directly\n    return node\n
    avl_tree.cpp
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode *rotate(TreeNode *node) {\n    // Get balance factor of node\n    int _balanceFactor = balanceFactor(node);\n    // Left-leaning tree\n    if (_balanceFactor > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (_balanceFactor < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.java
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode rotate(TreeNode node) {\n    // Get balance factor of node\n    int balanceFactor = balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = leftRotate(node.left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = rightRotate(node.right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.cs
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode? Rotate(TreeNode? node) {\n    // Get balance factor of node\n    int balanceFactorInt = BalanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactorInt > 1) {\n        if (BalanceFactor(node?.left) >= 0) {\n            // Right rotation\n            return RightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node!.left = LeftRotate(node!.left);\n            return RightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactorInt < -1) {\n        if (BalanceFactor(node?.right) <= 0) {\n            // Left rotation\n            return LeftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node!.right = RightRotate(node!.right);\n            return LeftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.go
    /* Perform rotation operation to restore balance to this subtree */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n    // Get balance factor of node\n    // Go recommends short variables, here bf refers to t.balanceFactor\n    bf := t.balanceFactor(node)\n    // Left-leaning tree\n    if bf > 1 {\n        if t.balanceFactor(node.Left) >= 0 {\n            // Right rotation\n            return t.rightRotate(node)\n        } else {\n            // First left rotation then right rotation\n            node.Left = t.leftRotate(node.Left)\n            return t.rightRotate(node)\n        }\n    }\n    // Right-leaning tree\n    if bf < -1 {\n        if t.balanceFactor(node.Right) <= 0 {\n            // Left rotation\n            return t.leftRotate(node)\n        } else {\n            // First right rotation then left rotation\n            node.Right = t.rightRotate(node.Right)\n            return t.leftRotate(node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
    avl_tree.swift
    /* Perform rotation operation to restore balance to this subtree */\nfunc rotate(node: TreeNode?) -> TreeNode? {\n    // Get balance factor of node\n    let balanceFactor = balanceFactor(node: node)\n    // Left-leaning tree\n    if balanceFactor > 1 {\n        if self.balanceFactor(node: node?.left) >= 0 {\n            // Right rotation\n            return rightRotate(node: node)\n        } else {\n            // First left rotation then right rotation\n            node?.left = leftRotate(node: node?.left)\n            return rightRotate(node: node)\n        }\n    }\n    // Right-leaning tree\n    if balanceFactor < -1 {\n        if self.balanceFactor(node: node?.right) <= 0 {\n            // Left rotation\n            return leftRotate(node: node)\n        } else {\n            // First right rotation then left rotation\n            node?.right = rightRotate(node: node?.right)\n            return leftRotate(node: node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
    avl_tree.js
    /* Perform rotation operation to restore balance to this subtree */\n#rotate(node) {\n    // Get balance factor of node\n    const balanceFactor = this.balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return this.#rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = this.#leftRotate(node.left);\n            return this.#rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return this.#leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = this.#rightRotate(node.right);\n            return this.#leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.ts
    /* Perform rotation operation to restore balance to this subtree */\nrotate(node: TreeNode): TreeNode {\n    // Get balance factor of node\n    const balanceFactor = this.balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return this.rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = this.leftRotate(node.left);\n            return this.rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return this.leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = this.rightRotate(node.right);\n            return this.leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.dart
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode? rotate(TreeNode? node) {\n  // Get balance factor of node\n  int factor = balanceFactor(node);\n  // Left-leaning tree\n  if (factor > 1) {\n    if (balanceFactor(node!.left) >= 0) {\n      // Right rotation\n      return rightRotate(node);\n    } else {\n      // First left rotation then right rotation\n      node.left = leftRotate(node.left);\n      return rightRotate(node);\n    }\n  }\n  // Right-leaning tree\n  if (factor < -1) {\n    if (balanceFactor(node!.right) <= 0) {\n      // Left rotation\n      return leftRotate(node);\n    } else {\n      // First right rotation then left rotation\n      node.right = rightRotate(node.right);\n      return leftRotate(node);\n    }\n  }\n  // Balanced tree, no rotation needed, return directly\n  return node;\n}\n
    avl_tree.rs
    /* Perform rotation operation to restore balance to this subtree */\nfn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    // Get balance factor of node\n    let balance_factor = Self::balance_factor(node.clone());\n    // Left-leaning tree\n    if balance_factor > 1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().left.clone()) >= 0 {\n            // Right rotation\n            Self::right_rotate(Some(node))\n        } else {\n            // First left rotation then right rotation\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    // Right-leaning tree\n    else if balance_factor < -1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().right.clone()) <= 0 {\n            // Left rotation\n            Self::left_rotate(Some(node))\n        } else {\n            // First right rotation then left rotation\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        // Balanced tree, no rotation needed, return directly\n        node\n    }\n}\n
    avl_tree.c
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode *rotate(TreeNode *node) {\n    // Get balance factor of node\n    int bf = balanceFactor(node);\n    // Left-leaning tree\n    if (bf > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (bf < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.kt
    /* Perform rotation operation to restore balance to this subtree */\nfun rotate(node: TreeNode): TreeNode {\n    // Get balance factor of node\n    val balanceFactor = balanceFactor(node)\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return rightRotate(node)\n        } else {\n            // First left rotation then right rotation\n            node.left = leftRotate(node.left)\n            return rightRotate(node)\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return leftRotate(node)\n        } else {\n            // First right rotation then left rotation\n            node.right = rightRotate(node.right)\n            return leftRotate(node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
    avl_tree.rb
    ### Perform rotation to rebalance subtree ###\ndef rotate(node)\n  # Get balance factor of node\n  balance_factor = balance_factor(node)\n  # Left-heavy tree\n  if balance_factor > 1\n    if balance_factor(node.left) >= 0\n      # Right rotation\n      return right_rotate(node)\n    else\n      # First left rotation then right rotation\n      node.left = left_rotate(node.left)\n      return right_rotate(node)\n    end\n  # Right-heavy tree\n  elsif balance_factor < -1\n    if balance_factor(node.right) <= 0\n      # Left rotation\n      return left_rotate(node)\n    else\n      # First right rotation then left rotation\n      node.right = right_rotate(node.right)\n      return left_rotate(node)\n    end\n  end\n  # Balanced tree, no rotation needed, return directly\n  node\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-common-operations-in-avl-trees","level":2,"title":"7.5.3   Common Operations in AVL Trees","text":"","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-node-insertion","level":3,"title":"1.   Node Insertion","text":"

    The node insertion operation in AVL trees is similar in principle to that in binary search trees. The only difference is that after inserting a node in an AVL tree, a series of unbalanced nodes may appear on the path from that node to the root. Therefore, we need to start from this node and perform rotation operations from bottom to top, restoring balance to all unbalanced nodes. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def insert(self, val):\n    \"\"\"Insert node\"\"\"\n    self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n    \"\"\"Recursively insert node (helper method)\"\"\"\n    if node is None:\n        return TreeNode(val)\n    # 1. Find insertion position and insert node\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        # Duplicate node not inserted, return directly\n        return node\n    # Update node height\n    self.update_height(node)\n    # 2. Perform rotation operation to restore balance to this subtree\n    return self.rotate(node)\n
    avl_tree.cpp
    /* Insert node */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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;    // Duplicate node not inserted, return directly\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.java
    /* Insert node */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode insertHelper(TreeNode node, int val) {\n    if (node == null)\n        return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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; // Duplicate node not inserted, return directly\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.cs
    /* Insert node */\nvoid Insert(int val) {\n    root = InsertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n    if (node == null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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;     // Duplicate node not inserted, return directly\n    UpdateHeight(node);  // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = Rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.go
    /* Insert node */\nfunc (t *aVLTree) insert(val int) {\n    t.root = t.insertHelper(t.root, val)\n}\n\n/* Recursively insert node (helper function) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return NewTreeNode(val)\n    }\n    /* 1. Find insertion position and insert node */\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        // Duplicate node not inserted, return directly\n        return node\n    }\n    // Update node height\n    t.updateHeight(node)\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = t.rotate(node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.swift
    /* Insert node */\nfunc insert(val: Int) {\n    root = insertHelper(node: root, val: val)\n}\n\n/* Recursively insert node (helper method) */\nfunc insertHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return TreeNode(x: val)\n    }\n    /* 1. Find insertion position and insert node */\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 // Duplicate node not inserted, return directly\n    }\n    updateHeight(node: node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node: node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.js
    /* Insert node */\ninsert(val) {\n    this.root = this.#insertHelper(this.root, val);\n}\n\n/* Recursively insert node (helper method) */\n#insertHelper(node, val) {\n    if (node === null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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; // Duplicate node not inserted, return directly\n    this.#updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.#rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.ts
    /* Insert node */\ninsert(val: number): void {\n    this.root = this.insertHelper(this.root, val);\n}\n\n/* Recursively insert node (helper method) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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; // Duplicate node not inserted, return directly\n    }\n    this.updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.dart
    /* Insert node */\nvoid insert(int val) {\n  root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n  if (node == null) return TreeNode(val);\n  /* 1. Find insertion position and insert node */\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; // Duplicate node not inserted, return directly\n  updateHeight(node); // Update node height\n  /* 2. Perform rotation operation to restore balance to this subtree */\n  node = rotate(node);\n  // Return root node of subtree\n  return node;\n}\n
    avl_tree.rs
    /* Insert node */\nfn insert(&mut self, val: i32) {\n    self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* Recursively insert node (helper method) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Find insertion position and insert node */\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); // Duplicate node not inserted, return directly\n                }\n            }\n            Self::update_height(Some(node.clone())); // Update node height\n\n            /* 2. Perform rotation operation to restore balance to this subtree */\n            node = Self::rotate(Some(node)).unwrap();\n            // Return root node of subtree\n            Some(node)\n        }\n        None => Some(TreeNode::new(val)),\n    }\n}\n
    avl_tree.c
    /* Insert node */\nvoid insert(AVLTree *tree, int val) {\n    tree->root = insertHelper(tree->root, val);\n}\n\n/* Recursively insert node (helper function) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == NULL) {\n        return newTreeNode(val);\n    }\n    /* 1. Find insertion position and insert node */\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        // Duplicate node not inserted, return directly\n        return node;\n    }\n    // Update node height\n    updateHeight(node);\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.kt
    /* Insert node */\nfun insert(_val: Int) {\n    root = insertHelper(root, _val)\n}\n\n/* Recursively insert node (helper method) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n    if (n == null)\n        return TreeNode(_val)\n    var node = n\n    /* 1. Find insertion position and insert node */\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 // Duplicate node not inserted, return directly\n    updateHeight(node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.rb
    ### Insert node ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n### Recursively insert node (helper method) ###\ndef insert_helper(node, val)\n  return TreeNode.new(val) if node.nil?\n  # 1. Find insertion position and insert node\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    # Duplicate node not inserted, return directly\n    return node\n  end\n  # Update node height\n  update_height(node)\n  # 2. Perform rotation operation to restore balance to this subtree\n  rotate(node)\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-node-removal","level":3,"title":"2.   Node Removal","text":"

    Similarly, on the basis of the binary search tree's node removal method, rotation operations need to be performed from bottom to top to restore balance to all unbalanced nodes. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def remove(self, val: int):\n    \"\"\"Delete node\"\"\"\n    self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n    \"\"\"Recursively delete node (helper method)\"\"\"\n    if node is None:\n        return None\n    # 1. Find node and delete\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            # Number of child nodes = 0, delete node directly and return\n            if child is None:\n                return None\n            # Number of child nodes = 1, delete node directly\n            else:\n                node = child\n        else:\n            # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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    # Update node height\n    self.update_height(node)\n    # 2. Perform rotation operation to restore balance to this subtree\n    return self.rotate(node)\n
    avl_tree.cpp
    /* Remove node */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return nullptr;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == nullptr) {\n                delete node;\n                return nullptr;\n            }\n            // Number of child nodes = 1, delete node directly\n            else {\n                delete node;\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.java
    /* Remove node */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode removeHelper(TreeNode node, int val) {\n    if (node == null)\n        return null;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null;\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.cs
    /* Remove node */\nvoid Remove(int val) {\n    root = RemoveHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n    if (node == null) return null;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null;\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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);  // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = Rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.go
    /* Remove node */\nfunc (t *aVLTree) remove(val int) {\n    t.root = t.removeHelper(t.root, val)\n}\n\n/* Recursively remove node (helper function) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return nil\n    }\n    /* 1. Find node and delete */\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                // Number of child nodes = 0, delete node directly and return\n                return nil\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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    // Update node height\n    t.updateHeight(node)\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = t.rotate(node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.swift
    /* Remove node */\nfunc remove(val: Int) {\n    root = removeHelper(node: root, val: val)\n}\n\n/* Recursively delete node (helper method) */\nfunc removeHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return nil\n    }\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if child == nil {\n                return nil\n            }\n            // Number of child nodes = 1, delete node directly\n            else {\n                node = child\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node: node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.js
    /* Remove node */\nremove(val) {\n    this.root = this.#removeHelper(this.root, val);\n}\n\n/* Recursively delete node (helper method) */\n#removeHelper(node, val) {\n    if (node === null) return null;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child === null) return null;\n            // Number of child nodes = 1, delete node directly\n            else node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.#rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.ts
    /* Remove node */\nremove(val: number): void {\n    this.root = this.removeHelper(this.root, val);\n}\n\n/* Recursively delete node (helper method) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return null;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child === null) {\n                return null;\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.dart
    /* Remove node */\nvoid remove(int val) {\n  root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n  if (node == null) return null;\n  /* 1. Find node and delete */\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      // Number of child nodes = 0, delete node directly and return\n      if (child == null)\n        return null;\n      // Number of child nodes = 1, delete node directly\n      else\n        node = child;\n    } else {\n      // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n  /* 2. Perform rotation operation to restore balance to this subtree */\n  node = rotate(node);\n  // Return root node of subtree\n  return node;\n}\n
    avl_tree.rs
    /* Remove node */\nfn remove(&self, val: i32) {\n    Self::remove_helper(self.root.clone(), val);\n}\n\n/* Recursively delete node (helper method) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Find node and delete */\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                    // Number of child nodes = 0, delete node directly and return\n                    None => {\n                        return None;\n                    }\n                    // Number of child nodes = 1, delete node directly\n                    Some(child) => node = child,\n                }\n            } else {\n                // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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())); // Update node height\n\n            /* 2. Perform rotation operation to restore balance to this subtree */\n            node = Self::rotate(Some(node)).unwrap();\n            // Return root node of subtree\n            Some(node)\n        }\n        None => None,\n    }\n}\n
    avl_tree.c
    /* Remove node */\n// Cannot use remove keyword here due to stdio.h inclusion\nvoid removeItem(AVLTree *tree, int val) {\n    TreeNode *root = removeHelper(tree->root, val);\n}\n\n/* Recursively remove node (helper function) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    TreeNode *child, *grandChild;\n    if (node == NULL) {\n        return NULL;\n    }\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == NULL) {\n                return NULL;\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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    // Update node height\n    updateHeight(node);\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.kt
    /* Remove node */\nfun remove(_val: Int) {\n    root = removeHelper(root, _val)\n}\n\n/* Recursively delete node (helper method) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n    var node = n ?: return null\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.rb
    ### Delete node ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n### Recursively delete node (helper method) ###\ndef remove_helper(node, val)\n  return if node.nil?\n  # 1. Find node and delete\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      # Number of child nodes = 0, delete node directly and return\n      return if child.nil?\n      # Number of child nodes = 1, delete node directly\n      node = child\n    else\n      # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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  # Update node height\n  update_height(node)\n  # 2. Perform rotation operation to restore balance to this subtree\n  rotate(node)\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3-node-search","level":3,"title":"3.   Node Search","text":"

    The node search operation in AVL trees is consistent with that in binary search trees, and will not be elaborated here.

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-typical-applications-of-avl-trees","level":2,"title":"7.5.4   Typical Applications of AVL Trees","text":"
    • Organizing and storing large-scale data, suitable for scenarios with high-frequency searches and low-frequency insertions and deletions.
    • Used to build index systems in databases.
    • Red-black trees are also a common type of balanced binary search tree. Compared to AVL trees, red-black trees have more relaxed balance conditions, require fewer rotation operations for node insertion and deletion, and have higher average efficiency for node addition and deletion operations.
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   Binary Search Tree","text":"

    As shown in Figure 7-16, a binary search tree satisfies the following conditions.

    1. For the root node, the value of all nodes in the left subtree \\(<\\) the value of the root node \\(<\\) the value of all nodes in the right subtree.
    2. The left and right subtrees of any node are also binary search trees, i.e., they satisfy condition 1. as well.

    Figure 7-16   Binary search tree

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741-operations-on-a-binary-search-tree","level":2,"title":"7.4.1   Operations on a Binary Search Tree","text":"

    We encapsulate the binary search tree as a class BinarySearchTree and declare a member variable root pointing to the tree's root node.

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1-searching-for-a-node","level":3,"title":"1.   Searching for a Node","text":"

    Given a target node value num, we can search according to the properties of the binary search tree. As shown in Figure 7-17, we declare a node cur and start from the binary search tree's root node root, looping to compare cur.val with num.

    • If cur.val < num, it means the target node is in cur's right subtree, thus execute cur = cur.right.
    • If cur.val > num, it means the target node is in cur's left subtree, thus execute cur = cur.left.
    • If cur.val = num, it means the target node is found, exit the loop, and return the node.
    <1><2><3><4>

    Figure 7-17   Example of searching for a node in a binary search tree

    The search operation in a binary search tree follows the same principle as binary search: each round rules out half of the remaining cases. The number of loop iterations is at most the height of the tree. When the tree is balanced, the search takes \\(O(\\log n)\\) time. The example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
    def search(self, num: int) -> TreeNode | None:\n    \"\"\"Search node\"\"\"\n    cur = self._root\n    # Loop search, exit after passing leaf node\n    while cur is not None:\n        # Target node is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Target node is in cur's left subtree\n        elif cur.val > num:\n            cur = cur.left\n        # Found target node, exit loop\n        else:\n            break\n    return cur\n
    binary_search_tree.cpp
    /* Search node */\nTreeNode *search(int num) {\n    TreeNode *cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Target node is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Target node is in cur's left subtree\n        else if (cur->val > num)\n            cur = cur->left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.java
    /* Search node */\nTreeNode search(int num) {\n    TreeNode cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num)\n            cur = cur.left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.cs
    /* Search node */\nTreeNode? Search(int num) {\n    TreeNode? cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur =\n            cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num)\n            cur = cur.left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.go
    /* Search node */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n    node := bst.root\n    // Loop search, exit after passing leaf node\n    for node != nil {\n        if node.Val.(int) < num {\n            // Target node is in cur's right subtree\n            node = node.Right\n        } else if node.Val.(int) > num {\n            // Target node is in cur's left subtree\n            node = node.Left\n        } else {\n            // Found target node, exit loop\n            break\n        }\n    }\n    // Return target node\n    return node\n}\n
    binary_search_tree.swift
    /* Search node */\nfunc search(num: Int) -> TreeNode? {\n    var cur = root\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Target node is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Target node is in cur's left subtree\n        else if cur!.val > num {\n            cur = cur?.left\n        }\n        // Found target node, exit loop\n        else {\n            break\n        }\n    }\n    // Return target node\n    return cur\n}\n
    binary_search_tree.js
    /* Search node */\nsearch(num) {\n    let cur = this.root;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num) cur = cur.left;\n        // Found target node, exit loop\n        else break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.ts
    /* Search node */\nsearch(num: number): TreeNode | null {\n    let cur = this.root;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num) cur = cur.left;\n        // Found target node, exit loop\n        else break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.dart
    /* Search node */\nTreeNode? search(int _num) {\n  TreeNode? cur = _root;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Target node is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Target node is in cur's left subtree\n    else if (cur.val > _num)\n      cur = cur.left;\n    // Found target node, exit loop\n    else\n      break;\n  }\n  // Return target node\n  return cur;\n}\n
    binary_search_tree.rs
    /* Search node */\npub fn search(&self, num: i32) -> OptionTreeNodeRc {\n    let mut cur = self.root.clone();\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Target node is in cur's right subtree\n            Ordering::Greater => cur = node.borrow().right.clone(),\n            // Target node is in cur's left subtree\n            Ordering::Less => cur = node.borrow().left.clone(),\n            // Found target node, exit loop\n            Ordering::Equal => break,\n        }\n    }\n\n    // Return target node\n    cur\n}\n
    binary_search_tree.c
    /* Search node */\nTreeNode *search(BinarySearchTree *bst, int num) {\n    TreeNode *cur = bst->root;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        if (cur->val < num) {\n            // Target node is in cur's right subtree\n            cur = cur->right;\n        } else if (cur->val > num) {\n            // Target node is in cur's left subtree\n            cur = cur->left;\n        } else {\n            // Found target node, exit loop\n            break;\n        }\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.kt
    /* Search node */\nfun search(num: Int): TreeNode? {\n    var cur = root\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Target node is in cur's left subtree\n        else if (cur._val > num)\n            cur.left\n        // Found target node, exit loop\n        else\n            break\n    }\n    // Return target node\n    return cur\n}\n
    binary_search_tree.rb
    ### Search node ###\ndef search(num)\n  cur = @root\n\n  # Loop search, exit after passing leaf node\n  while !cur.nil?\n    # Target node is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Target node is in cur's left subtree\n    elsif cur.val > num\n      cur = cur.left\n    # Found target node, exit loop\n    else\n      break\n    end\n  end\n\n  cur\nend\n
    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2-inserting-a-node","level":3,"title":"2.   Inserting a Node","text":"

    Given an element num to be inserted, in order to maintain the property of the binary search tree \"left subtree < root node < right subtree,\" the insertion process is as shown in Figure 7-18.

    1. Finding the insertion position: Similar to the search operation, start from the root node and loop downward searching according to the size relationship between the current node value and num, until passing the leaf node (traversing to None) and then exit the loop.
    2. Insert the node at that position: Create a node for num and place it at the None position.

    Figure 7-18   Inserting a node into a binary search tree

    In the code implementation, note the following two points:

    • Binary search trees do not allow duplicate nodes; otherwise, the tree would no longer satisfy its definition. Therefore, if the node to be inserted already exists in the tree, the insertion is skipped and the function returns directly.
    • To implement the node insertion, we need to use node pre to save the node from the previous loop iteration. This way, when traversing to None, we can obtain its parent node, thereby completing the node insertion operation.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
    def insert(self, num: int):\n    \"\"\"Insert node\"\"\"\n    # If tree is empty, initialize root node\n    if self._root is None:\n        self._root = TreeNode(num)\n        return\n    # Loop search, exit after passing leaf node\n    cur, pre = self._root, None\n    while cur is not None:\n        # Found duplicate node, return directly\n        if cur.val == num:\n            return\n        pre = cur\n        # Insertion position is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Insertion position is in cur's left subtree\n        else:\n            cur = cur.left\n    # Insert node\n    node = TreeNode(num)\n    if pre.val < num:\n        pre.right = node\n    else:\n        pre.left = node\n
    binary_search_tree.cpp
    /* Insert node */\nvoid insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == nullptr) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode *cur = root, *pre = nullptr;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Found duplicate node, return directly\n        if (cur->val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur->left;\n    }\n    // Insert node\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
    /* Insert node */\nvoid insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // Insert node\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
    /* Insert node */\nvoid Insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode? cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n\n    // Insert node\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
    /* Insert node */\nfunc (bst *binarySearchTree) insert(num int) {\n    cur := bst.root\n    // If tree is empty, initialize root node\n    if cur == nil {\n        bst.root = NewTreeNode(num)\n        return\n    }\n    // Node position before the node to be inserted\n    var pre *TreeNode = nil\n    // Loop search, exit after passing leaf node\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    // Insert node\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
    /* Insert node */\nfunc insert(num: Int) {\n    // If tree is empty, initialize root node\n    if root == nil {\n        root = TreeNode(x: num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Found duplicate node, return directly\n        if cur!.val == num {\n            return\n        }\n        pre = cur\n        // Insertion position is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Insertion position is in cur's left subtree\n        else {\n            cur = cur?.left\n        }\n    }\n    // Insert node\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
    /* Insert node */\ninsert(num) {\n    // If tree is empty, initialize root node\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur = this.root,\n        pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found duplicate node, return directly\n        if (cur.val === num) return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else cur = cur.left;\n    }\n    // Insert node\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
    /* Insert node */\ninsert(num: number): void {\n    // If tree is empty, initialize root node\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    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found duplicate node, return directly\n        if (cur.val === num) return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else cur = cur.left;\n    }\n    // Insert node\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
    /* Insert node */\nvoid insert(int _num) {\n  // If tree is empty, initialize root node\n  if (_root == null) {\n    _root = TreeNode(_num);\n    return;\n  }\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Found duplicate node, return directly\n    if (cur.val == _num) return;\n    pre = cur;\n    // Insertion position is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Insertion position is in cur's left subtree\n    else\n      cur = cur.left;\n  }\n  // Insert node\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
    /* Insert node */\npub fn insert(&mut self, num: i32) {\n    // If tree is empty, initialize root node\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    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Found duplicate node, return directly\n            Ordering::Equal => return,\n            // Insertion position is in cur's right subtree\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Insertion position is in cur's left subtree\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // Insert node\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
    /* Insert node */\nvoid insert(BinarySearchTree *bst, int num) {\n    // If tree is empty, initialize root node\n    if (bst->root == NULL) {\n        bst->root = newTreeNode(num);\n        return;\n    }\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        // Found duplicate node, return directly\n        if (cur->val == num) {\n            return;\n        }\n        pre = cur;\n        if (cur->val < num) {\n            // Insertion position is in cur's right subtree\n            cur = cur->right;\n        } else {\n            // Insertion position is in cur's left subtree\n            cur = cur->left;\n        }\n    }\n    // Insert node\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
    /* Insert node */\nfun insert(num: Int) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = TreeNode(num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode? = null\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur._val == num)\n            return\n        pre = cur\n        // Insertion position is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Insertion position is in cur's left subtree\n        else\n            cur.left\n    }\n    // Insert node\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
    ### Insert node ###\ndef insert(num)\n  # If tree is empty, initialize root node\n  if @root.nil?\n    @root = TreeNode.new(num)\n    return\n  end\n\n  # Loop search, exit after passing leaf node\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Found duplicate node, return directly\n    return if cur.val == num\n\n    pre = cur\n    # Insertion position is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Insertion position is in cur's left subtree\n    else\n      cur = cur.left\n    end\n  end\n\n  # Insert node\n  node = TreeNode.new(num)\n  if pre.val < num\n    pre.right = node\n  else\n    pre.left = node\n  end\nend\n

    Similar to searching for a node, inserting a node uses \\(O(\\log n)\\) time.

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3-removing-a-node","level":3,"title":"3.   Removing a Node","text":"

    First, find the target node in the binary search tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of \"left subtree \\(<\\) root node \\(<\\) right subtree\" is still maintained. Therefore, depending on the number of child nodes the target node has, we consider three cases: degree \\(0\\), degree \\(1\\), and degree \\(2\\), and perform the corresponding removal operation.

    As shown in Figure 7-19, when the degree of the node to be removed is \\(0\\), it means the node is a leaf node and can be directly removed.

    Figure 7-19   Removing a node in a binary search tree (degree 0)

    As shown in Figure 7-20, when the degree of the node to be removed is \\(1\\), replacing the node to be removed with its child node is sufficient.

    Figure 7-20   Removing a node in a binary search tree (degree 1)

    When the degree of the node to be removed is \\(2\\), we cannot directly remove it; instead, we need to use a node to replace it. To maintain the binary search tree's property of \"left subtree \\(<\\) root node \\(<\\) right subtree,\" this node can be either the smallest node in the right subtree or the largest node in the left subtree.

    Assuming we choose the smallest node in the right subtree, that is, the inorder successor, the removal process is as shown in Figure 7-21.

    1. Find the next node of the node to be removed in the \"inorder traversal sequence,\" denoted as tmp.
    2. Replace the value of the node to be removed with the value of tmp, and recursively remove node tmp in the tree.
    <1><2><3><4>

    Figure 7-21   Removing a node in a binary search tree (degree 2)

    The node removal operation also uses \\(O(\\log n)\\) time, where finding the node to be removed requires \\(O(\\log n)\\) time, and obtaining the inorder successor node requires \\(O(\\log n)\\) time. Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
    def remove(self, num: int):\n    \"\"\"Delete node\"\"\"\n    # If tree is empty, return directly\n    if self._root is None:\n        return\n    # Loop search, exit after passing leaf node\n    cur, pre = self._root, None\n    while cur is not None:\n        # Found node to delete, exit loop\n        if cur.val == num:\n            break\n        pre = cur\n        # Node to delete is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Node to delete is in cur's left subtree\n        else:\n            cur = cur.left\n    # If no node to delete, return directly\n    if cur is None:\n        return\n\n    # Number of child nodes = 0 or 1\n    if cur.left is None or cur.right is None:\n        # When number of child nodes = 0 / 1, child = null / that child node\n        child = cur.left or cur.right\n        # Delete node cur\n        if cur != self._root:\n            if pre.left == cur:\n                pre.left = child\n            else:\n                pre.right = child\n        else:\n            # If deleted node is root node, reassign root node\n            self._root = child\n    # Number of child nodes = 2\n    else:\n        # Get next node of cur in inorder traversal\n        tmp: TreeNode = cur.right\n        while tmp.left is not None:\n            tmp = tmp.left\n        # Recursively delete node tmp\n        self.remove(tmp.val)\n        # Replace cur with tmp\n        cur.val = tmp.val\n
    binary_search_tree.cpp
    /* Remove node */\nvoid remove(int num) {\n    // If tree is empty, return directly\n    if (root == nullptr)\n        return;\n    TreeNode *cur = root, *pre = nullptr;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Found node to delete, exit loop\n        if (cur->val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur->left;\n    }\n    // If no node to delete, return directly\n    if (cur == nullptr)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur->left == nullptr || cur->right == nullptr) {\n        // When number of child nodes = 0 / 1, child = nullptr / that child node\n        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre->left == cur)\n                pre->left = child;\n            else\n                pre->right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n        // Free memory\n        delete cur;\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode *tmp = cur->right;\n        while (tmp->left != nullptr) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Recursively delete node tmp\n        remove(tmp->val);\n        // Replace cur with tmp\n        cur->val = tmpVal;\n    }\n}\n
    binary_search_tree.java
    /* Remove node */\nvoid remove(int num) {\n    // If tree is empty, return directly\n    if (root == null)\n        return;\n    TreeNode cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        TreeNode child = cur.left != null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        remove(tmp.val);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
    binary_search_tree.cs
    /* Remove node */\nvoid Remove(int num) {\n    // If tree is empty, return directly\n    if (root == null)\n        return;\n    TreeNode? cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        TreeNode? child = cur.left ?? cur.right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre!.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode? tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        Remove(tmp.val!.Value);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
    binary_search_tree.go
    /* Remove node */\nfunc (bst *binarySearchTree) remove(num int) {\n    cur := bst.root\n    // If tree is empty, return directly\n    if cur == nil {\n        return\n    }\n    // Node position before the node to be removed\n    var pre *TreeNode = nil\n    // Loop search, exit after passing leaf node\n    for cur != nil {\n        if cur.Val == num {\n            break\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            // Node to be removed is in right subtree\n            cur = cur.Right\n        } else {\n            // Node to be removed is in left subtree\n            cur = cur.Left\n        }\n    }\n    // If no node to delete, return directly\n    if cur == nil {\n        return\n    }\n    // Number of child nodes is 0 or 1\n    if cur.Left == nil || cur.Right == nil {\n        var child *TreeNode = nil\n        // Get child node of node to be removed\n        if cur.Left != nil {\n            child = cur.Left\n        } else {\n            child = cur.Right\n        }\n        // Delete node 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            // If deleted node is root node, reassign root node\n            bst.root = child\n        }\n        // Number of child nodes is 2\n    } else {\n        // Get next node of node cur to be removed in in-order traversal\n        tmp := cur.Right\n        for tmp.Left != nil {\n            tmp = tmp.Left\n        }\n        // Recursively delete node tmp\n        bst.remove(tmp.Val.(int))\n        // Replace cur with tmp\n        cur.Val = tmp.Val\n    }\n}\n
    binary_search_tree.swift
    /* Remove node */\nfunc remove(num: Int) {\n    // If tree is empty, return directly\n    if root == nil {\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Found node to delete, exit loop\n        if cur!.val == num {\n            break\n        }\n        pre = cur\n        // Node to delete is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Node to delete is in cur's left subtree\n        else {\n            cur = cur?.left\n        }\n    }\n    // If no node to delete, return directly\n    if cur == nil {\n        return\n    }\n    // Number of child nodes = 0 or 1\n    if cur?.left == nil || cur?.right == nil {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        let child = cur?.left ?? cur?.right\n        // Delete node cur\n        if cur !== root {\n            if pre?.left === cur {\n                pre?.left = child\n            } else {\n                pre?.right = child\n            }\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        var tmp = cur?.right\n        while tmp?.left != nil {\n            tmp = tmp?.left\n        }\n        // Recursively delete node tmp\n        remove(num: tmp!.val)\n        // Replace cur with tmp\n        cur?.val = tmp!.val\n    }\n}\n
    binary_search_tree.js
    /* Remove node */\nremove(num) {\n    // If tree is empty, return directly\n    if (this.root === null) return;\n    let cur = this.root,\n        pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found node to delete, exit loop\n        if (cur.val === num) break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur === null) return;\n    // Number of child nodes = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        const child = cur.left !== null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur !== this.root) {\n            if (pre.left === cur) pre.left = child;\n            else pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            this.root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        let tmp = cur.right;\n        while (tmp.left !== null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        this.remove(tmp.val);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
    binary_search_tree.ts
    /* Remove node */\nremove(num: number): void {\n    // If tree is empty, return directly\n    if (this.root === null) return;\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found node to delete, exit loop\n        if (cur.val === num) break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur === null) return;\n    // Number of child nodes = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        const child: TreeNode | null =\n            cur.left !== null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur !== this.root) {\n            if (pre!.left === cur) pre!.left = child;\n            else pre!.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            this.root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        let tmp: TreeNode | null = cur.right;\n        while (tmp!.left !== null) {\n            tmp = tmp!.left;\n        }\n        // Recursively delete node tmp\n        this.remove(tmp!.val);\n        // Replace cur with tmp\n        cur.val = tmp!.val;\n    }\n}\n
    binary_search_tree.dart
    /* Remove node */\nvoid remove(int _num) {\n  // If tree is empty, return directly\n  if (_root == null) return;\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Found node to delete, exit loop\n    if (cur.val == _num) break;\n    pre = cur;\n    // Node to delete is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Node to delete is in cur's left subtree\n    else\n      cur = cur.left;\n  }\n  // If no node to delete, return directly\n  if (cur == null) return;\n  // Number of child nodes = 0 or 1\n  if (cur.left == null || cur.right == null) {\n    // When number of child nodes = 0 / 1, child = null / that child node\n    TreeNode? child = cur.left ?? cur.right;\n    // Delete node cur\n    if (cur != _root) {\n      if (pre!.left == cur)\n        pre.left = child;\n      else\n        pre.right = child;\n    } else {\n      // If deleted node is root node, reassign root node\n      _root = child;\n    }\n  } else {\n    // Number of child nodes = 2\n    // Get next node of cur in inorder traversal\n    TreeNode? tmp = cur.right;\n    while (tmp!.left != null) {\n      tmp = tmp.left;\n    }\n    // Recursively delete node tmp\n    remove(tmp.val);\n    // Replace cur with tmp\n    cur.val = tmp.val;\n  }\n}\n
    binary_search_tree.rs
    /* Remove node */\npub fn remove(&mut self, num: i32) {\n    // If tree is empty, return directly\n    if self.root.is_none() {\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Found node to delete, exit loop\n            Ordering::Equal => break,\n            // Node to delete is in cur's right subtree\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Node to delete is in cur's left subtree\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // If no node to delete, return directly\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        // Number of child nodes = 0 or 1\n        (None, None) | (Some(_), None) | (None, Some(_)) => {\n            // When number of child nodes = 0 / 1, child = nullptr / that child node\n            let child = left_child.or(right_child);\n            let pre = pre.unwrap();\n            // Delete node 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                // If deleted node is root node, reassign root node\n                self.root = child;\n            }\n        }\n        // Number of child nodes = 2\n        (Some(_), Some(_)) => {\n            // Get next node of cur in inorder traversal\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            // Recursively delete node tmp\n            self.remove(tmp_val);\n            // Replace cur with tmp\n            cur.borrow_mut().val = tmp_val;\n        }\n    }\n}\n
    binary_search_tree.c
    /* Remove node */\n// Cannot use remove keyword here due to stdio.h inclusion\nvoid removeItem(BinarySearchTree *bst, int num) {\n    // If tree is empty, return directly\n    if (bst->root == NULL)\n        return;\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        // Found node to delete, exit loop\n        if (cur->val == num)\n            break;\n        pre = cur;\n        if (cur->val < num) {\n            // Node to delete is in right subtree of root\n            cur = cur->right;\n        } else {\n            // Node to delete is in left subtree of root\n            cur = cur->left;\n        }\n    }\n    // If no node to delete, return directly\n    if (cur == NULL)\n        return;\n    // Check if node to delete has children\n    if (cur->left == NULL || cur->right == NULL) {\n        /* Number of child nodes = 0 or 1 */\n        // When number of child nodes = 0 / 1, child = nullptr / that child node\n        TreeNode *child = cur->left != NULL ? cur->left : cur->right;\n        // Delete node cur\n        if (pre->left == cur) {\n            pre->left = child;\n        } else {\n            pre->right = child;\n        }\n        // Free memory\n        free(cur);\n    } else {\n        /* Number of child nodes = 2 */\n        // Get next node of cur in inorder traversal\n        TreeNode *tmp = cur->right;\n        while (tmp->left != NULL) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Recursively delete node tmp\n        removeItem(bst, tmp->val);\n        // Replace cur with tmp\n        cur->val = tmpVal;\n    }\n}\n
    binary_search_tree.kt
    /* Remove node */\nfun remove(num: Int) {\n    // If tree is empty, return directly\n    if (root == null)\n        return\n    var cur = root\n    var pre: TreeNode? = null\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur._val == num)\n            break\n        pre = cur\n        // Node to delete is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Node to delete is in cur's left subtree\n        else\n            cur.left\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        val child = if (cur.left != null)\n            cur.left\n        else\n            cur.right\n        // Delete node cur\n        if (cur != root) {\n            if (pre!!.left == cur)\n                pre.left = child\n            else\n                pre.right = child\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child\n        }\n        // Number of child nodes = 2\n    } else {\n        // Get next node of cur in inorder traversal\n        var tmp = cur.right\n        while (tmp!!.left != null) {\n            tmp = tmp.left\n        }\n        // Recursively delete node tmp\n        remove(tmp._val)\n        // Replace cur with tmp\n        cur._val = tmp._val\n    }\n}\n
    binary_search_tree.rb
    ### Delete node ###\ndef remove(num)\n  # If tree is empty, return directly\n  return if @root.nil?\n\n  # Loop search, exit after passing leaf node\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Found node to delete, exit loop\n    break if cur.val == num\n\n    pre = cur\n    # Node to delete is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Node to delete is in cur's left subtree\n    else\n      cur = cur.left\n    end\n  end\n  # If no node to delete, return directly\n  return if cur.nil?\n\n  # Number of child nodes = 0 or 1\n  if cur.left.nil? || cur.right.nil?\n    # When number of child nodes = 0 / 1, child = null / that child node\n    child = cur.left || cur.right\n    # Delete node 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      # If deleted node is root node, reassign root node\n      @root = child\n    end\n  # Number of child nodes = 2\n  else\n    # Get next node of cur in inorder traversal\n    tmp = cur.right\n    while !tmp.left.nil?\n      tmp = tmp.left\n    end\n    # Recursively delete node tmp\n    remove(tmp.val)\n    # Replace cur with tmp\n    cur.val = tmp.val\n  end\nend\n
    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4-inorder-traversal-is-ordered","level":3,"title":"4.   Inorder Traversal Is Ordered","text":"

    As shown in Figure 7-22, the inorder traversal of a binary tree follows the \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\" traversal order, while the binary search tree satisfies the \"left child node \\(<\\) root node \\(<\\) right child node\" size relationship.

    This means that when performing an inorder traversal in a binary search tree, the next smallest node is always traversed first, thus yielding an important property: The inorder traversal sequence of a binary search tree is ascending.

    Using the property of inorder traversal being ascending, we can obtain ordered data in a binary search tree in only \\(O(n)\\) time, without the need for additional sorting operations, which is very efficient.

    Figure 7-22   Inorder traversal sequence of a binary search tree

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742-efficiency-of-binary-search-trees","level":2,"title":"7.4.2   Efficiency of Binary Search Trees","text":"

    Given a set of data, we consider using an array or a binary search tree for storage. Observing Table 7-2, all operations in a binary search tree have logarithmic time complexity, providing stable and efficient performance. Arrays are more efficient than binary search trees only in scenarios with high-frequency additions and low-frequency searches and deletions.

    Table 7-2   Efficiency comparison between arrays and search trees

    Unsorted array Binary search tree Search element \\(O(n)\\) \\(O(\\log n)\\) Insert element \\(O(1)\\) \\(O(\\log n)\\) Remove element \\(O(n)\\) \\(O(\\log n)\\)

    In the ideal case, a binary search tree is balanced, so any node can be found within \\(O(\\log n)\\) loop iterations.

    However, if we continuously insert and remove nodes in a binary search tree, it may degenerate into a linked list as shown in Figure 7-23, where the time complexity of various operations also degrades to \\(O(n)\\).

    Figure 7-23   Degradation of a binary search tree

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743-common-applications-of-binary-search-trees","level":2,"title":"7.4.3   Common Applications of Binary Search Trees","text":"
    • Used as multi-level indexes in systems to implement efficient search, insertion, and removal operations.
    • Serves as the underlying data structure for certain search algorithms.
    • Used to store data streams to maintain their ordered state.
    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   Binary Tree","text":"

    A binary tree is a non-linear data structure that models the hierarchical relationship between \"ancestors\" and \"descendants\" and embodies a divide-and-conquer pattern in which each split branches into two. Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class TreeNode:\n    \"\"\"Binary tree node\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Node value\n        self.left: TreeNode | None = None  # Reference to left child node\n        self.right: TreeNode | None = None # Reference to right child node\n
    /* Binary tree node */\nstruct TreeNode {\n    int val;          // Node value\n    TreeNode *left;   // Pointer to left child node\n    TreeNode *right;  // Pointer to right child node\n    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n
    /* Binary tree node */\nclass TreeNode {\n    int val;         // Node value\n    TreeNode left;   // Reference to left child node\n    TreeNode right;  // Reference to right child node\n    TreeNode(int x) { val = x; }\n}\n
    /* Binary tree node */\nclass TreeNode(int? x) {\n    public int? val = x;    // Node value\n    public TreeNode? left;  // Reference to left child node\n    public TreeNode? right; // Reference to right child node\n}\n
    /* Binary tree node */\ntype TreeNode struct {\n    Val   int\n    Left  *TreeNode\n    Right *TreeNode\n}\n/* Constructor */\nfunc NewTreeNode(v int) *TreeNode {\n    return &TreeNode{\n        Left:  nil, // Pointer to left child node\n        Right: nil, // Pointer to right child node\n        Val:   v,   // Node value\n    }\n}\n
    /* Binary tree node */\nclass TreeNode {\n    var val: Int // Node value\n    var left: TreeNode? // Reference to left child node\n    var right: TreeNode? // Reference to right child node\n\n    init(x: Int) {\n        val = x\n    }\n}\n
    /* Binary tree node */\nclass TreeNode {\n    val; // Node value\n    left; // Pointer to left child node\n    right; // Pointer to right child node\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
    /* Binary tree node */\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; // Node value\n        this.left = left === undefined ? null : left; // Reference to left child node\n        this.right = right === undefined ? null : right; // Reference to right child node\n    }\n}\n
    /* Binary tree node */\nclass TreeNode {\n  int val;         // Node value\n  TreeNode? left;  // Reference to left child node\n  TreeNode? right; // Reference to right child node\n  TreeNode(this.val, [this.left, this.right]);\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Binary tree node */\nstruct TreeNode {\n    val: i32,                               // Node value\n    left: Option<Rc<RefCell<TreeNode>>>,    // Reference to left child node\n    right: Option<Rc<RefCell<TreeNode>>>,   // Reference to right child node\n}\n\nimpl TreeNode {\n    /* Constructor */\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
    /* Binary tree node */\ntypedef struct TreeNode {\n    int val;                // Node value\n    int height;             // Node height\n    struct TreeNode *left;  // Pointer to left child node\n    struct TreeNode *right; // Pointer to right child node\n} TreeNode;\n\n/* Constructor */\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
    /* Binary tree node */\nclass TreeNode(val _val: Int) {  // Node value\n    val left: TreeNode? = null   // Reference to left child node\n    val right: TreeNode? = null  // Reference to right child node\n}\n
    ### Binary tree node class ###\nclass TreeNode\n  attr_accessor :val    # Node value\n  attr_accessor :left   # Reference to left child node\n  attr_accessor :right  # Reference to right child node\n\n  def initialize(val)\n    @val = val\n  end\nend\n

    Each node has two references (pointers), pointing respectively to the left-child node and right-child node. This node is called the parent node of these two child nodes. When given a node of a binary tree, we call the tree formed by this node's left child and all nodes below it the left subtree of this node. Similarly, the right subtree can be defined.

    In a binary tree, every non-leaf node has child nodes and therefore non-empty subtrees. As shown in Figure 7-1, if \"Node 2\" is regarded as a parent node, its left and right child nodes are \"Node 4\" and \"Node 5\" respectively. The left subtree is formed by \"Node 4\" and all nodes beneath it, while the right subtree is formed by \"Node 5\" and all nodes beneath it.

    Figure 7-1   Parent Node, child Node, subtree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#711-common-terminology-of-binary-trees","level":2,"title":"7.1.1   Common Terminology of Binary Trees","text":"

    The commonly used terminology of binary trees is shown in Figure 7-2.

    • Root node: The node at the top level of a binary tree, which does not have a parent node.
    • Leaf node: A node that does not have any child nodes, with both of its pointers pointing to None.
    • Edge: A line segment that connects two nodes, representing a reference (pointer) between the nodes.
    • The level of a node: It increases from top to bottom, with the root node being at level 1.
    • The degree of a node: The number of child nodes that a node has. In a binary tree, the degree can be 0, 1, or 2.
    • The height of a binary tree: The number of edges from the root node to the farthest leaf node.
    • The depth of a node: The number of edges from the root node to the node.
    • The height of a node: The number of edges from the farthest leaf node to the node.

    Figure 7-2   Common Terminology of Binary Trees

    Tip

    We usually define \"height\" and \"depth\" as the number of edges traversed, but some textbooks and problem statements define them as the number of nodes on the path. In that case, both values are larger by 1.

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#712-basic-operations-of-binary-trees","level":2,"title":"7.1.2   Basic Operations of Binary Trees","text":"","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#1-initializing-a-binary-tree","level":3,"title":"1.   Initializing a Binary Tree","text":"

    Similar to a linked list, the initialization of a binary tree involves first creating the nodes and then establishing the references (pointers) between them.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
    # Initializing a binary tree\n# Initializing nodes\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
    binary_tree.cpp
    /* Initializing a binary tree */\n// Initializing nodes\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// Linking references (pointers) between nodes\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
    binary_tree.java
    // Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.cs
    /* Initializing a binary tree */\n// Initializing nodes\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.go
    /* Initializing a binary tree */\n// Initializing nodes\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// Linking references (pointers) between nodes\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n
    binary_tree.swift
    // Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
    binary_tree.js
    /* Initializing a binary tree */\n// Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.ts
    /* Initializing a binary tree */\n// Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.dart
    /* Initializing a binary tree */\n// Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.rs
    // Initializing nodes\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// Linking references (pointers) between nodes\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
    /* Initializing a binary tree */\n// Initializing nodes\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// Linking references (pointers) between nodes\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
    binary_tree.kt
    // Initializing nodes\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
    binary_tree.rb
    # Initializing a binary tree\n# Initializing nodes\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#2-inserting-and-removing-nodes","level":3,"title":"2.   Inserting and Removing Nodes","text":"

    Similar to a linked list, inserting and removing nodes in a binary tree can be achieved by modifying pointers. Figure 7-3 provides an example.

    Figure 7-3   Inserting and removing nodes in a binary tree

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
    # Inserting and removing nodes\np = TreeNode(0)\n# Inserting node P between n1 -> n2\nn1.left = p\np.left = n2\n# Removing node P\nn1.left = n2\n
    binary_tree.cpp
    /* Inserting and removing nodes */\nTreeNode* P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1->left = P;\nP->left = n2;\n// Removing node P\nn1->left = n2;\n
    binary_tree.java
    TreeNode P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.cs
    /* Inserting and removing nodes */\nTreeNode P = new(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.go
    /* Inserting and removing nodes */\n// Inserting node P between n1 and n2\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// Removing node P\nn1.Left = n2\n
    binary_tree.swift
    let P = TreeNode(x: 0)\n// Inserting node P between n1 and n2\nn1.left = P\nP.left = n2\n// Removing node P\nn1.left = n2\n
    binary_tree.js
    /* Inserting and removing nodes */\nlet P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.ts
    /* Inserting and removing nodes */\nconst P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.dart
    /* Inserting and removing nodes */\nTreeNode P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.rs
    let p = TreeNode::new(0);\n// Inserting node P between n1 and n2\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// Removing node P\nn1.borrow_mut().left = Some(n2);\n
    binary_tree.c
    /* Inserting and removing nodes */\nTreeNode *P = newTreeNode(0);\n// Inserting node P between n1 and n2\nn1->left = P;\nP->left = n2;\n// Removing node P\nn1->left = n2;\n
    binary_tree.kt
    val P = TreeNode(0)\n// Inserting node P between n1 and n2\nn1.left = P\nP.left = n2\n// Removing node P\nn1.left = n2\n
    binary_tree.rb
    # Inserting and removing nodes\n_p = TreeNode.new(0)\n# Inserting node _p between n1 and n2\nn1.left = _p\n_p.left = n2\n# Removing node _p\nn1.left = n2\n
    Code Visualization

    Full Screen >

    Tip

    Keep in mind that inserting a node can alter the original logical structure of a binary tree, while deleting a node usually entails removing that node together with its entire subtree. In practice, insertion and deletion in binary trees are therefore typically implemented as coordinated sequences of operations to achieve a meaningful result.

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#713-common-types-of-binary-trees","level":2,"title":"7.1.3   Common Types of Binary Trees","text":"","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#1-perfect-binary-tree","level":3,"title":"1.   Perfect Binary Tree","text":"

    As shown in Figure 7-4, a perfect binary tree has every level completely filled. In a perfect binary tree, leaf nodes have a degree of \\(0\\), while all other nodes have a degree of \\(2\\). If the tree height is \\(h\\), the total number of nodes is \\(2^{h+1} - 1\\), following a standard exponential pattern that mirrors the common phenomenon of cell division in nature.

    Tip

    Please note that in the Chinese community, a perfect binary tree is often referred to as a full binary tree.

    Figure 7-4   Perfect binary tree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#2-complete-binary-tree","level":3,"title":"2.   Complete Binary Tree","text":"

    As shown in Figure 7-5, a complete binary tree only allows the bottom level to be incompletely filled, and the nodes at the bottom level must be filled continuously from left to right. Note that a perfect binary tree is also a complete binary tree.

    Figure 7-5   Complete binary tree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#3-full-binary-tree","level":3,"title":"3.   Full Binary Tree","text":"

    As shown in Figure 7-6, in a full binary tree, all nodes except leaf nodes have two child nodes.

    Figure 7-6   Full binary tree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#4-balanced-binary-tree","level":3,"title":"4.   Balanced Binary Tree","text":"

    As shown in Figure 7-7, in a balanced binary tree, the absolute difference between the height of the left and right subtrees of any node does not exceed 1.

    Figure 7-7   Balanced binary tree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#714-degeneration-of-binary-trees","level":2,"title":"7.1.4   Degeneration of Binary Trees","text":"

    Figure 7-8 contrasts the ideal and degenerate structures of binary trees. When every level is filled, the tree becomes a \"perfect binary tree\"; when all nodes skew to one side, the binary tree degenerates into a \"linked list\".

    • A perfect binary tree is the ideal case, fully leveraging the divide-and-conquer advantages of binary trees.
    • A linked list represents the other extreme, where all operations become linear operations with time complexity degrading to \\(O(n)\\).

    Figure 7-8   The Best and Worst Structures of Binary Trees

    As shown in Table 7-1, in the best and worst structures, the binary tree achieves either maximum or minimum values for leaf node count, total number of nodes, and height.

    Table 7-1   The Best and Worst Structures of Binary Trees

    Perfect binary tree Linked list Number of nodes at level \\(i\\) \\(2^{i-1}\\) \\(1\\) Number of leaf nodes in a tree with height \\(h\\) \\(2^h\\) \\(1\\) Total number of nodes in a tree with height \\(h\\) \\(2^{h+1} - 1\\) \\(h + 1\\) Height of a tree with \\(n\\) total nodes \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   Binary Tree Traversal","text":"

    From a physical structure perspective, a tree is a data structure based on linked lists. Hence, its traversal method involves accessing nodes one by one through pointers. However, a tree is a non-linear data structure, which makes traversing a tree more complex than traversing a linked list, requiring the assistance of search algorithms.

    The common traversal methods for binary trees include level-order traversal, pre-order traversal, in-order traversal, and post-order traversal.

    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721-level-order-traversal","level":2,"title":"7.2.1   Level-Order Traversal","text":"

    As shown in Figure 7-9, level-order traversal traverses the binary tree from top to bottom, layer by layer. Within each level, it visits nodes from left to right.

    Level-order traversal is essentially breadth-first traversal, also known as breadth-first search (BFS), which proceeds outward level by level.

    Figure 7-9   Level-order traversal of a binary tree

    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1-code-implementation","level":3,"title":"1.   Code Implementation","text":"

    Breadth-first traversal is typically implemented with the help of a \"queue\". The queue follows the \"first in, first out\" rule, while breadth-first traversal follows the \"layer-by-layer progression\" rule; the underlying ideas of the two are consistent. The implementation code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py
    def level_order(root: TreeNode | None) -> list[int]:\n    \"\"\"Level-order traversal\"\"\"\n    # Initialize queue, add root node\n    queue: deque[TreeNode] = deque()\n    queue.append(root)\n    # Initialize a list to save the traversal sequence\n    res = []\n    while queue:\n        node: TreeNode = queue.popleft()  # Dequeue\n        res.append(node.val)  # Save node value\n        if node.left is not None:\n            queue.append(node.left)  # Left child node enqueue\n        if node.right is not None:\n            queue.append(node.right)  # Right child node enqueue\n    return res\n
    binary_tree_bfs.cpp
    /* Level-order traversal */\nvector<int> levelOrder(TreeNode *root) {\n    // Initialize queue, add root node\n    queue<TreeNode *> queue;\n    queue.push(root);\n    // Initialize a list to save the traversal sequence\n    vector<int> vec;\n    while (!queue.empty()) {\n        TreeNode *node = queue.front();\n        queue.pop();              // Dequeue\n        vec.push_back(node->val); // Save node value\n        if (node->left != nullptr)\n            queue.push(node->left); // Left child node enqueue\n        if (node->right != nullptr)\n            queue.push(node->right); // Right child node enqueue\n    }\n    return vec;\n}\n
    binary_tree_bfs.java
    /* Level-order traversal */\nList<Integer> levelOrder(TreeNode root) {\n    // Initialize queue, add root node\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root);\n    // Initialize a list to save the traversal sequence\n    List<Integer> list = new ArrayList<>();\n    while (!queue.isEmpty()) {\n        TreeNode node = queue.poll(); // Dequeue\n        list.add(node.val);           // Save node value\n        if (node.left != null)\n            queue.offer(node.left);   // Left child node enqueue\n        if (node.right != null)\n            queue.offer(node.right);  // Right child node enqueue\n    }\n    return list;\n}\n
    binary_tree_bfs.cs
    /* Level-order traversal */\nList<int> LevelOrder(TreeNode root) {\n    // Initialize queue, add root node\n    Queue<TreeNode> queue = new();\n    queue.Enqueue(root);\n    // Initialize a list to save the traversal sequence\n    List<int> list = [];\n    while (queue.Count != 0) {\n        TreeNode node = queue.Dequeue(); // Dequeue\n        list.Add(node.val!.Value);       // Save node value\n        if (node.left != null)\n            queue.Enqueue(node.left);    // Left child node enqueue\n        if (node.right != null)\n            queue.Enqueue(node.right);   // Right child node enqueue\n    }\n    return list;\n}\n
    binary_tree_bfs.go
    /* Level-order traversal */\nfunc levelOrder(root *TreeNode) []any {\n    // Initialize queue, add root node\n    queue := list.New()\n    queue.PushBack(root)\n    // Initialize a slice to save traversal sequence\n    nums := make([]any, 0)\n    for queue.Len() > 0 {\n        // Dequeue\n        node := queue.Remove(queue.Front()).(*TreeNode)\n        // Save node value\n        nums = append(nums, node.Val)\n        if node.Left != nil {\n            // Left child node enqueue\n            queue.PushBack(node.Left)\n        }\n        if node.Right != nil {\n            // Right child node enqueue\n            queue.PushBack(node.Right)\n        }\n    }\n    return nums\n}\n
    binary_tree_bfs.swift
    /* Level-order traversal */\nfunc levelOrder(root: TreeNode) -> [Int] {\n    // Initialize queue, add root node\n    var queue: [TreeNode] = [root]\n    // Initialize a list to save the traversal sequence\n    var list: [Int] = []\n    while !queue.isEmpty {\n        let node = queue.removeFirst() // Dequeue\n        list.append(node.val) // Save node value\n        if let left = node.left {\n            queue.append(left) // Left child node enqueue\n        }\n        if let right = node.right {\n            queue.append(right) // Right child node enqueue\n        }\n    }\n    return list\n}\n
    binary_tree_bfs.js
    /* Level-order traversal */\nfunction levelOrder(root) {\n    // Initialize queue, add root node\n    const queue = [root];\n    // Initialize a list to save the traversal sequence\n    const list = [];\n    while (queue.length) {\n        let node = queue.shift(); // Dequeue\n        list.push(node.val); // Save node value\n        if (node.left) queue.push(node.left); // Left child node enqueue\n        if (node.right) queue.push(node.right); // Right child node enqueue\n    }\n    return list;\n}\n
    binary_tree_bfs.ts
    /* Level-order traversal */\nfunction levelOrder(root: TreeNode | null): number[] {\n    // Initialize queue, add root node\n    const queue = [root];\n    // Initialize a list to save the traversal sequence\n    const list: number[] = [];\n    while (queue.length) {\n        let node = queue.shift() as TreeNode; // Dequeue\n        list.push(node.val); // Save node value\n        if (node.left) {\n            queue.push(node.left); // Left child node enqueue\n        }\n        if (node.right) {\n            queue.push(node.right); // Right child node enqueue\n        }\n    }\n    return list;\n}\n
    binary_tree_bfs.dart
    /* Level-order traversal */\nList<int> levelOrder(TreeNode? root) {\n  // Initialize queue, add root node\n  Queue<TreeNode?> queue = Queue();\n  queue.add(root);\n  // Initialize a list to save the traversal sequence\n  List<int> res = [];\n  while (queue.isNotEmpty) {\n    TreeNode? node = queue.removeFirst(); // Dequeue\n    res.add(node!.val); // Save node value\n    if (node.left != null) queue.add(node.left); // Left child node enqueue\n    if (node.right != null) queue.add(node.right); // Right child node enqueue\n  }\n  return res;\n}\n
    binary_tree_bfs.rs
    /* Level-order traversal */\nfn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {\n    // Initialize queue, add root node\n    let mut que = VecDeque::new();\n    que.push_back(root.clone());\n    // Initialize a list to save the traversal sequence\n    let mut vec = Vec::new();\n\n    while let Some(node) = que.pop_front() {\n        // Dequeue\n        vec.push(node.borrow().val); // Save node value\n        if let Some(left) = node.borrow().left.as_ref() {\n            que.push_back(left.clone()); // Left child node enqueue\n        }\n        if let Some(right) = node.borrow().right.as_ref() {\n            que.push_back(right.clone()); // Right child node enqueue\n        };\n    }\n    vec\n}\n
    binary_tree_bfs.c
    /* Level-order traversal */\nint *levelOrder(TreeNode *root, int *size) {\n    /* Auxiliary queue */\n    int front, rear;\n    int index, *arr;\n    TreeNode *node;\n    TreeNode **queue;\n\n    /* Auxiliary queue */\n    queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n    // Queue pointer\n    front = 0, rear = 0;\n    // Add root node\n    queue[rear++] = root;\n    // Initialize a list to save the traversal sequence\n    /* Auxiliary array */\n    arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n    // Array pointer\n    index = 0;\n    while (front < rear) {\n        // Dequeue\n        node = queue[front++];\n        // Save node value\n        arr[index++] = node->val;\n        if (node->left != NULL) {\n            // Left child node enqueue\n            queue[rear++] = node->left;\n        }\n        if (node->right != NULL) {\n            // Right child node enqueue\n            queue[rear++] = node->right;\n        }\n    }\n    // Update array length value\n    *size = index;\n    arr = realloc(arr, sizeof(int) * (*size));\n\n    // Free auxiliary array space\n    free(queue);\n    return arr;\n}\n
    binary_tree_bfs.kt
    /* Level-order traversal */\nfun levelOrder(root: TreeNode?): MutableList<Int> {\n    // Initialize queue, add root node\n    val queue = LinkedList<TreeNode?>()\n    queue.add(root)\n    // Initialize a list to save the traversal sequence\n    val list = mutableListOf<Int>()\n    while (queue.isNotEmpty()) {\n        val node = queue.poll()      // Dequeue\n        list.add(node?._val!!)       // Save node value\n        if (node.left != null)\n            queue.offer(node.left)   // Left child node enqueue\n        if (node.right != null)\n            queue.offer(node.right)  // Right child node enqueue\n    }\n    return list\n}\n
    binary_tree_bfs.rb
    ### Level-order traversal ###\ndef level_order(root)\n  # Initialize queue, add root node\n  queue = [root]\n  # Initialize a list to save the traversal sequence\n  res = []\n  while !queue.empty?\n    node = queue.shift # Dequeue\n    res << node.val # Save node value\n    queue << node.left unless node.left.nil? # Left child node enqueue\n    queue << node.right unless node.right.nil? # Right child node enqueue\n  end\n  res\nend\n
    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2-complexity-analysis","level":3,"title":"2.   Complexity Analysis","text":"
    • Time complexity is \\(O(n)\\): All nodes are visited once, using \\(O(n)\\) time, where \\(n\\) is the number of nodes.
    • Space complexity is \\(O(n)\\): In the worst case, i.e., a full binary tree, before traversing to the bottom level, the queue contains at most \\((n + 1) / 2\\) nodes simultaneously, occupying \\(O(n)\\) space.
    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722-preorder-inorder-and-postorder-traversal","level":2,"title":"7.2.2   Preorder, Inorder, and Postorder Traversal","text":"

    Correspondingly, preorder, inorder, and postorder traversals all belong to depth-first traversal, also known as depth-first search (DFS), which goes as deep as possible before backtracking.

    Figure 7-10 shows how depth-first traversal works on a binary tree. Depth-first traversal is like \"walking\" around the perimeter of the entire binary tree, encountering three positions at each node, corresponding to preorder, inorder, and postorder traversal.

    Figure 7-10   Preorder, inorder, and postorder traversal of a binary tree

    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1-code-implementation_1","level":3,"title":"1.   Code Implementation","text":"

    Depth-first search is usually implemented based on recursion:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py
    def pre_order(root: TreeNode | None):\n    \"\"\"Preorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: root node -> left subtree -> right subtree\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    \"\"\"Inorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: left subtree -> root node -> right subtree\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    \"\"\"Postorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: left subtree -> right subtree -> root node\n    post_order(root=root.left)\n    post_order(root=root.right)\n    res.append(root.val)\n
    binary_tree_dfs.cpp
    /* Preorder traversal */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    vec.push_back(root->val);\n    preOrder(root->left);\n    preOrder(root->right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root->left);\n    vec.push_back(root->val);\n    inOrder(root->right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root->left);\n    postOrder(root->right);\n    vec.push_back(root->val);\n}\n
    binary_tree_dfs.java
    /* Preorder traversal */\nvoid preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.add(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.add(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.add(root.val);\n}\n
    binary_tree_dfs.cs
    /* Preorder traversal */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.Add(root.val!.Value);\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n\n/* Inorder traversal */\nvoid InOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: left subtree -> root node -> right subtree\n    InOrder(root.left);\n    list.Add(root.val!.Value);\n    InOrder(root.right);\n}\n\n/* Postorder traversal */\nvoid PostOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: left subtree -> right subtree -> root node\n    PostOrder(root.left);\n    PostOrder(root.right);\n    list.Add(root.val!.Value);\n}\n
    binary_tree_dfs.go
    /* Preorder traversal */\nfunc preOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    nums = append(nums, node.Val)\n    preOrder(node.Left)\n    preOrder(node.Right)\n}\n\n/* Inorder traversal */\nfunc inOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(node.Left)\n    nums = append(nums, node.Val)\n    inOrder(node.Right)\n}\n\n/* Postorder traversal */\nfunc postOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(node.Left)\n    postOrder(node.Right)\n    nums = append(nums, node.Val)\n}\n
    binary_tree_dfs.swift
    /* Preorder traversal */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    list.append(root.val)\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n\n/* Inorder traversal */\nfunc inOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root: root.left)\n    list.append(root.val)\n    inOrder(root: root.right)\n}\n\n/* Postorder traversal */\nfunc postOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root: root.left)\n    postOrder(root: root.right)\n    list.append(root.val)\n}\n
    binary_tree_dfs.js
    /* Preorder traversal */\nfunction preOrder(root) {\n    if (root === null) return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nfunction inOrder(root) {\n    if (root === null) return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nfunction postOrder(root) {\n    if (root === null) return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
    binary_tree_dfs.ts
    /* Preorder traversal */\nfunction preOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nfunction inOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nfunction postOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
    binary_tree_dfs.dart
    /* Preorder traversal */\nvoid preOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: root node -> left subtree -> right subtree\n  list.add(node.val);\n  preOrder(node.left);\n  preOrder(node.right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: left subtree -> root node -> right subtree\n  inOrder(node.left);\n  list.add(node.val);\n  inOrder(node.right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: left subtree -> right subtree -> root node\n  postOrder(node.left);\n  postOrder(node.right);\n  list.add(node.val);\n}\n
    binary_tree_dfs.rs
    /* Preorder traversal */\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            // Visit priority: root node -> left subtree -> right subtree\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/* Inorder traversal */\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            // Visit priority: left subtree -> root node -> right subtree\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/* Postorder traversal */\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            // Visit priority: left subtree -> right subtree -> root node\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
    /* Preorder traversal */\nvoid preOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    arr[(*size)++] = root->val;\n    preOrder(root->left, size);\n    preOrder(root->right, size);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root->left, size);\n    arr[(*size)++] = root->val;\n    inOrder(root->right, size);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root->left, size);\n    postOrder(root->right, size);\n    arr[(*size)++] = root->val;\n}\n
    binary_tree_dfs.kt
    /* Preorder traversal */\nfun preOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: root node -> left subtree -> right subtree\n    list.add(root._val)\n    preOrder(root.left)\n    preOrder(root.right)\n}\n\n/* Inorder traversal */\nfun inOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left)\n    list.add(root._val)\n    inOrder(root.right)\n}\n\n/* Postorder traversal */\nfun postOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left)\n    postOrder(root.right)\n    list.add(root._val)\n}\n
    binary_tree_dfs.rb
    ### Pre-order traversal ###\ndef pre_order(root)\n  return if root.nil?\n\n  # Visit priority: root node -> left subtree -> right subtree\n  $res << root.val\n  pre_order(root.left)\n  pre_order(root.right)\nend\n\n### In-order traversal ###\ndef in_order(root)\n  return if root.nil?\n\n  # Visit priority: left subtree -> root node -> right subtree\n  in_order(root.left)\n  $res << root.val\n  in_order(root.right)\nend\n\n### Post-order traversal ###\ndef post_order(root)\n  return if root.nil?\n\n  # Visit priority: left subtree -> right subtree -> root node\n  post_order(root.left)\n  post_order(root.right)\n  $res << root.val\nend\n

    Tip

    Depth-first search can also be implemented iteratively, and interested readers can explore this on their own.

    Figure 7-11 shows the recursive process of preorder traversal of a binary tree, which can be divided into two opposite phases: \"descending\" and \"returning\".

    1. \"Descending\" means making a new recursive call, during which the program visits the next node.
    2. \"Returning\" means the function call returns, indicating that the current node has been fully processed.
    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 7-11   The recursive process of preorder traversal

    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2-complexity-analysis_1","level":3,"title":"2.   Complexity Analysis","text":"
    • Time complexity is \\(O(n)\\): All nodes are visited once, using \\(O(n)\\) time.
    • Space complexity is \\(O(n)\\): In the worst case, i.e., the tree degenerates into a linked list, the recursion depth reaches \\(n\\), and the system occupies \\(O(n)\\) stack frame space.
    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   Summary","text":"","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]},{"location":"chapter_tree/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of splitting into two. Each binary tree node contains a value and two pointers, which point to its left and right child nodes.
    • For a certain node in a binary tree, the tree formed by its left (right) child node and all nodes below is called the left (right) subtree of that node.
    • Related terminology of binary trees includes root node, leaf node, level, degree, edge, height, and depth.
    • The initialization, node insertion, and node removal operations of binary trees are similar to those of linked lists.
    • Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. A perfect binary tree is the ideal form, while a linked list represents the worst degenerate case.
    • A binary tree can be represented using an array by arranging node values and empty slots in level-order traversal sequence, and implementing pointers based on the index mapping relationship between parent and child nodes.
    • Level-order traversal of a binary tree is a breadth-first search method that proceeds level by level, typically implemented using a queue.
    • Preorder, inorder, and postorder traversals all belong to depth-first search, which proceeds by going as deep as possible before backtracking, typically using recursion.
    • A binary search tree is an efficient data structure for element searching, with search, insertion, and removal operations all having time complexity of \\(O(\\log n)\\). When a binary search tree degenerates into a linked list, all time complexities degrade to \\(O(n)\\).
    • An AVL tree, also known as a balanced binary search tree, ensures the tree remains balanced after continuous node insertions and removals through rotation operations.
    • Rotation operations in AVL trees include right rotation, left rotation, right rotation followed by left rotation, and left rotation followed by right rotation. After inserting or removing nodes, AVL trees perform rotations from bottom to top to restore balance.
    ","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: For a binary tree with only one node, are both the height of the tree and the depth of the root node \\(0\\)?

    Yes, because height and depth are typically defined as the number of edges on the path.

    Q: The insertion and removal in a binary tree are generally accomplished by a set of operations. What does \"a set of operations\" refer to here? Does it imply releasing the resources of the child nodes?

    Taking the binary search tree as an example, the operation of removing a node needs to be handled in three different scenarios, each requiring multiple steps of node operations.

    Q: Why does DFS traversal of binary trees have three orders: preorder, inorder, and postorder, and what are their uses?

    Similar to forward and reverse traversal of arrays, preorder, inorder, and postorder traversals are three methods of binary tree traversal that allow us to obtain a traversal result in a specific order. For example, in a binary search tree, since nodes satisfy the relationship left child node value < root node value < right child node value, we only need to traverse the tree with the priority of \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\" to obtain an ordered node sequence.

    Q: In a right rotation operation handling the relationship between unbalanced nodes node, child, and grand_child, doesn't the connection between node and its parent node get lost after the right rotation?

    We need to view this problem from a recursive perspective. The right rotation operation right_rotate(root) passes in the root node of the subtree and eventually returns the root node of the subtree after rotation with return child. The connection between the subtree's root node and its parent node is completed after the function returns, which is not within the maintenance scope of the right rotation operation.

    Q: In C++, functions are divided into private and public sections. What considerations are there for this? Why are the height() function and the updateHeight() function placed in public and private, respectively?

    It mainly depends on the method's usage scope. If a method is only used within the class, then it is designed as private. For example, calling updateHeight() alone by the user makes no sense, as it is only a step in insertion or removal operations. However, height() is used to access node height, similar to vector.size(), so it is set to public for ease of use.

    Q: How do you build a binary search tree from a set of input data? Is the choice of root node very important?

    Yes, the method for building a tree is provided in the build_tree() method in the binary search tree code. As for the choice of root node, we typically sort the input data, then select the middle element as the root node, and recursively build the left and right subtrees. This approach maximizes the tree's balance.

    Q: In Java, do you always have to use the equals() method for string comparison?

    In Java, for primitive data types, == is used to compare whether the values of two variables are equal. For reference types, the working principles of the two symbols are different.

    • ==: Used to compare whether two variables point to the same object, i.e., whether their positions in memory are the same.
    • equals(): Used to compare whether the values of two objects are equal.

    Therefore, if we want to compare values, we should use equals(). However, strings initialized via String a = \"hi\"; String b = \"hi\"; are stored in the string constant pool and point to the same object, so a == b can also be used to compare the contents of the two strings.

    Q: Before reaching the bottom level, is the number of nodes in the queue \\(2^h\\) in breadth-first traversal?

    Yes, for example, a full binary tree with height \\(h = 2\\) has a total of \\(n = 7\\) nodes, then the bottom level has \\(4 = 2^h = (n + 1) / 2\\) nodes.

    ","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]}]} \ No newline at end of file +{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"Chapter 16.   Appendix","text":"","path":["Chapter 16. Appendix","Chapter 16.   Appendix"],"tags":[]},{"location":"chapter_appendix/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 16.1   Programming Environment Installation
    • 16.2   Contributing Together
    • 16.3   Glossary
    ","path":["Chapter 16. Appendix","Chapter 16.   Appendix"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   Contributing Together","text":"

    Due to limited capacity, there may be inevitable omissions and errors in this book. We appreciate your understanding and are grateful for your help in correcting them. If you discover typos, broken links, missing content, ambiguous wording, unclear explanations, or structural issues, please help us make corrections to provide readers with higher-quality learning resources.

    The GitHub IDs of all contributors will be displayed on the homepage of the book repository, the web version, and the PDF version to acknowledge their selfless contributions to the open source community.

    The Charm of Open Source

    The interval between two printings of a physical book is often quite long, making content updates very inconvenient.

    In this open source book, the time for content updates has been shortened to just days or even hours.

    ","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#1-minor-content-adjustments","level":3,"title":"1.   Minor Content Adjustments","text":"

    As shown in Figure 16-3, there is an \"edit icon\" in the top-right corner of each page. You can modify text or code by following these steps.

    1. Click the \"edit icon\". If you encounter a prompt asking you to \"Fork this repository\", please approve the operation.
    2. Modify the content of the Markdown source file, verify the correctness of the content, and maintain consistent formatting as much as possible.
    3. Fill in a description of your changes at the bottom of the page, then click the \"Propose file change\" button. After the new page loads, click the \"Create pull request\" button to submit your pull request.

    Figure 16-3   Page edit button

    Images cannot be directly modified. Please describe the issue by creating a new Issue or leaving a comment. We will promptly redraw and replace the images.

    ","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#2-content-creation","level":3,"title":"2.   Content Creation","text":"

    If you are interested in contributing to this open source project, including translating code into other programming languages or expanding article content, you will need to follow the Pull Request workflow below.

    1. Log in to GitHub and Fork the book's code repository to your personal account.
    2. Go to your forked repository page and use the git clone command to clone the repository to your local machine.
    3. Create content locally and conduct comprehensive tests to verify code correctness.
    4. Commit your local changes and push them to the remote repository.
    5. Refresh the repository webpage and click the \"Create pull request\" button to submit your pull request.
    ","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker-deployment","level":3,"title":"3.   Docker Deployment","text":"

    From the root directory of hello-algo, run the following Docker command to access the project at http://localhost:8000:

    docker-compose up -d\n

    Use the following command to remove the deployment:

    docker-compose down\n
    ","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   Programming Environment Installation","text":"","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1611-installing-ide","level":2,"title":"16.1.1   Installing IDE","text":"

    We recommend using the open-source and lightweight VS Code as the local integrated development environment (IDE). Visit the VS Code official website, and download and install the appropriate version of VS Code according to your operating system.

    Figure 16-1   Download VS Code from the Official Website

    VS Code has a powerful ecosystem of extensions that supports running and debugging most programming languages. For example, after installing the \"Python Extension Pack\" extension, you can debug Python code. The installation steps are shown in the following figure.

    Figure 16-2   Install VS Code Extensions

    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1612-installing-language-environments","level":2,"title":"16.1.2   Installing Language Environments","text":"","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1-python-environment","level":3,"title":"1.   Python Environment","text":"
    1. Download and install Miniconda3 with Python 3.10 or later.
    2. Search for python in the VS Code extension marketplace and install the Python Extension Pack.
    3. (Optional) Enter pip install black on the command line to install the code formatter.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc-environment","level":3,"title":"2.   C/C++ Environment","text":"
    1. Windows systems need to install MinGW (configuration tutorial); macOS comes with Clang built-in and does not require installation.
    2. Search for c++ in the VS Code extension marketplace and install the C/C++ Extension Pack.
    3. (Optional) Open the Settings page, search for the Clang_format_fallback Style code formatting option, and set it to { BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#3-java-environment","level":3,"title":"3.   Java Environment","text":"
    1. Download and install OpenJDK (version 10 or later).
    2. Search for java in the VS Code extension marketplace and install the Extension Pack for Java.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#4-c-environment","level":3,"title":"4.   C# Environment","text":"
    1. Download and install .NET 8.0.
    2. Search for C# Dev Kit in the VS Code extension marketplace and install C# Dev Kit (configuration tutorial).
    3. You can also use Visual Studio (installation tutorial).
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#5-go-environment","level":3,"title":"5.   Go Environment","text":"
    1. Download and install Go.
    2. Search for go in the VS Code extension marketplace and install Go.
    3. Press Ctrl + Shift + P to open the command palette, type go, select Go: Install/Update Tools, check all options and install.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift-environment","level":3,"title":"6.   Swift Environment","text":"
    1. Download and install Swift.
    2. Search for swift in the VS Code extension marketplace and install Swift for Visual Studio Code.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript-environment","level":3,"title":"7.   JavaScript Environment","text":"
    1. Download and install Node.js.
    2. (Optional) Search for Prettier in the VS Code extension marketplace and install the code formatter.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript-environment","level":3,"title":"8.   TypeScript Environment","text":"
    1. Follow the same installation steps as the JavaScript environment.
    2. Install TypeScript Execute (tsx).
    3. Search for typescript in the VS Code extension marketplace and install Pretty TypeScript Errors.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart-environment","level":3,"title":"9.   Dart Environment","text":"
    1. Download and install Dart.
    2. Search for dart in the VS Code extension marketplace and install Dart.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust-environment","level":3,"title":"10.   Rust Environment","text":"
    1. Download and install Rust.
    2. Search for rust in the VS Code extension marketplace and install rust-analyzer.
    ","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   Glossary","text":"

    The following table lists important terms that appear in this book.

    Table 16-1   Important Terms in Data Structures and Algorithms

    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 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 binary search tree balanced binary search tree balance factor heap max heap min heap priority queue heapify top-\\(k\\) problem 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 dynamic programming initial state state-transition equation knapsack problem edit distance problem greedy algorithm","path":["Chapter 16. Appendix","16.3   Glossary"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"Chapter 4.   Arrays and Linked Lists","text":"

    Abstract

    The world of data structures is like a solid brick wall.

    The bricks of an array are neatly aligned, each pressed tightly against the next. The bricks of a linked list are scattered about, with connecting vines weaving freely through the gaps between them.

    ","path":["Chapter 4. Arrays and Linked Lists","Chapter 4.   Arrays and Linked Lists"],"tags":[]},{"location":"chapter_array_and_linkedlist/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 4.1   Array
    • 4.2   Linked List
    • 4.3   List
    • 4.4   Random-Access Memory and Cache *
    • 4.5   Summary
    ","path":["Chapter 4. Arrays and Linked Lists","Chapter 4.   Arrays and Linked Lists"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   Array","text":"

    An array is a linear data structure that stores elements of the same type in contiguous memory space. The position of an element in the array is called the element's index. Figure 4-1 illustrates the main concepts and storage method of arrays.

    Figure 4-1   Array definition and storage method

    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411-common-array-operations","level":2,"title":"4.1.1   Common Array Operations","text":"","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1-initializing-arrays","level":3,"title":"1.   Initializing Arrays","text":"

    We can choose between two array initialization methods based on our needs: with or without initial values. When no initial values are specified, most programming languages initialize array elements to \\(0\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    # Initialize array\narr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n
    array.cpp
    /* Initialize array */\n// Stored on stack\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// Stored on heap (requires manual memory release)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n
    array.java
    /* Initialize array */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n
    array.cs
    /* Initialize array */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n
    array.go
    /* Initialize array */\nvar arr [5]int\n// In Go, specifying length ([5]int) creates an array; not specifying length ([]int) creates a slice\n// Since Go's arrays are designed to have their length determined at compile time, only constants can be used to specify the length\n// For convenience in implementing the extend() method, slices are treated as arrays below\nnums := []int{1, 3, 2, 5, 4}\n
    array.swift
    /* Initialize array */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n
    array.js
    /* Initialize array */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n
    array.ts
    /* Initialize array */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n
    array.dart
    /* Initialize array */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n
    array.rs
    /* Initialize array */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// In Rust, specifying length ([i32; 5]) creates an array; not specifying length (&[i32]) creates a slice\n// Since Rust's arrays are designed to have their length determined at compile time, only constants can be used to specify the length\n// Vector is the type generally used as a dynamic array in Rust\n// For convenience in implementing the extend() method, vectors are treated as arrays below\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
    array.c
    /* Initialize array */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n
    array.kt
    /* Initialize array */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n
    array.rb
    # Initialize array\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2-accessing-elements","level":3,"title":"2.   Accessing Elements","text":"

    Array elements are stored in contiguous memory space, which means calculating the memory address of array elements is very easy. Given the array's memory address (the memory address of the first element) and an element's index, we can use the formula shown in Figure 4-2 to calculate the element's memory address and directly access that element.

    Figure 4-2   Memory address calculation for array elements

    Observing Figure 4-2, we find that the first element of an array has an index of \\(0\\), which may seem counterintuitive since counting from \\(1\\) would be more natural. However, from the perspective of the address calculation formula, an index is essentially an offset from the memory address. The address offset of the first element is \\(0\\), so it is reasonable for its index to be \\(0\\).

    Accessing elements in an array is highly efficient; we can randomly access any element in the array in \\(O(1)\\) time.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def random_access(nums: list[int]) -> int:\n    \"\"\"Random access to element\"\"\"\n    # Randomly select a number from the interval [0, len(nums)-1]\n    random_index = random.randint(0, len(nums) - 1)\n    # Retrieve and return the random element\n    random_num = nums[random_index]\n    return random_num\n
    array.cpp
    /* Random access to element */\nint randomAccess(int *nums, int size) {\n    // Randomly select a number from interval [0, size)\n    int randomIndex = rand() % size;\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
    array.java
    /* Random access to element */\nint randomAccess(int[] nums) {\n    // Randomly select a number in the interval [0, nums.length)\n    int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
    array.cs
    /* Random access to element */\nint RandomAccess(int[] nums) {\n    Random random = new();\n    // Randomly select a number in interval [0, nums.Length)\n    int randomIndex = random.Next(nums.Length);\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
    array.go
    /* Random access to element */\nfunc randomAccess(nums []int) (randomNum int) {\n    // Randomly select a number in the interval [0, nums.length)\n    randomIndex := rand.Intn(len(nums))\n    // Retrieve and return the random element\n    randomNum = nums[randomIndex]\n    return\n}\n
    array.swift
    /* Random access to element */\nfunc randomAccess(nums: [Int]) -> Int {\n    // Randomly select a number in interval [0, nums.count)\n    let randomIndex = nums.indices.randomElement()!\n    // Retrieve and return the random element\n    let randomNum = nums[randomIndex]\n    return randomNum\n}\n
    array.js
    /* Random access to element */\nfunction randomAccess(nums) {\n    // Randomly select a number in the interval [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Retrieve and return the random element\n    const random_num = nums[random_index];\n    return random_num;\n}\n
    array.ts
    /* Random access to element */\nfunction randomAccess(nums: number[]): number {\n    // Randomly select a number in the interval [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Retrieve and return the random element\n    const random_num = nums[random_index];\n    return random_num;\n}\n
    array.dart
    /* Random access to element */\nint randomAccess(List<int> nums) {\n  // Randomly select a number in the interval [0, nums.length)\n  int randomIndex = Random().nextInt(nums.length);\n  // Retrieve and return the random element\n  int randomNum = nums[randomIndex];\n  return randomNum;\n}\n
    array.rs
    /* Random access to element */\nfn random_access(nums: &[i32]) -> i32 {\n    // Randomly select a number in interval [0, nums.len())\n    let random_index = rand::thread_rng().gen_range(0..nums.len());\n    // Retrieve and return the random element\n    let random_num = nums[random_index];\n    random_num\n}\n
    array.c
    /* Random access to element */\nint randomAccess(int *nums, int size) {\n    // Randomly select a number from interval [0, size)\n    int randomIndex = rand() % size;\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
    array.kt
    /* Random access to element */\nfun randomAccess(nums: IntArray): Int {\n    // Randomly select a number in interval [0, nums.size)\n    val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n    // Retrieve and return the random element\n    val randomNum = nums[randomIndex]\n    return randomNum\n}\n
    array.rb
    ### Random access element ###\ndef random_access(nums)\n  # Randomly select a number in the interval [0, nums.length)\n  random_index = Random.rand(0...nums.length)\n\n  # Retrieve and return the random element\n  nums[random_index]\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3-inserting-elements","level":3,"title":"3.   Inserting Elements","text":"

    Array elements are packed tightly together in memory, with no extra space between them for additional data. As shown in Figure 4-3, if we want to insert an element in the middle of an array, we need to shift all subsequent elements one position to the right and then assign the value at that index.

    Figure 4-3   Example of inserting an element into an array

    It is worth noting that since the length of an array is fixed, inserting an element will inevitably push the last element out of the array. We will leave the solution to this problem for discussion in the \"List\" chapter.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def insert(nums: list[int], num: int, index: int):\n    \"\"\"Insert element num at index index in the array\"\"\"\n    # Move all elements at and after index index backward by one position\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # Assign num to the element at index index\n    nums[index] = num\n
    array.cpp
    /* Insert element num at index index in the array */\nvoid insert(int *nums, int size, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.java
    /* Insert element num at index index in the array */\nvoid insert(int[] nums, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.cs
    /* Insert element num at index index in the array */\nvoid Insert(int[] nums, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.go
    /* Insert element num at index index in the array */\nfunc insert(nums []int, num int, index int) {\n    // Move all elements at and after index index backward by one position\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
    array.swift
    /* Insert element num at index index in the array */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // Move all elements at and after index index backward by one position\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
    array.js
    /* Insert element num at index index in the array */\nfunction insert(nums, num, index) {\n    // Move all elements at and after index index backward by one position\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.ts
    /* Insert element num at index index in the array */\nfunction insert(nums: number[], num: number, index: number): void {\n    // Move all elements at and after index index backward by one position\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.dart
    /* Insert element _num at array index index */\nvoid insert(List<int> nums, int _num, int index) {\n  // Move all elements at and after index index backward by one position\n  for (var i = nums.length - 1; i > index; i--) {\n    nums[i] = nums[i - 1];\n  }\n  // Assign _num to element at index\n  nums[index] = _num;\n}\n
    array.rs
    /* Insert element num at index index in the array */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // Move all elements at and after index index backward by one position\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.c
    /* Insert element num at index index in the array */\nvoid insert(int *nums, int size, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
    array.kt
    /* Insert element num at index index in the array */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // Move all elements at and after index index backward by one position\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
    array.rb
    ### Insert element num at index in array ###\ndef insert(nums, num, index)\n  # Move all elements at and after index index backward by one position\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # Assign num to the element at index index\n  nums[index] = num\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4-removing-elements","level":3,"title":"4.   Removing Elements","text":"

    Similarly, as shown in Figure 4-4, to delete the element at index \\(i\\), we need to shift all elements after index \\(i\\) forward by one position.

    Figure 4-4   Example of removing an element from an array

    Note that after the deletion is complete, the original last element is no longer meaningful, so we do not need to modify it explicitly.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def remove(nums: list[int], index: int):\n    \"\"\"Remove the element at index index\"\"\"\n    # Move all elements after index index forward by one position\n    for i in range(index, len(nums) - 1):\n        nums[i] = nums[i + 1]\n
    array.cpp
    /* Remove the element at index index */\nvoid remove(int *nums, int size, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.java
    /* Remove the element at index index */\nvoid remove(int[] nums, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.cs
    /* Remove the element at index index */\nvoid Remove(int[] nums, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < nums.Length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.go
    /* Remove the element at index index */\nfunc remove(nums []int, index int) {\n    // Move all elements after index index forward by one position\n    for i := index; i < len(nums)-1; i++ {\n        nums[i] = nums[i+1]\n    }\n}\n
    array.swift
    /* Remove the element at index index */\nfunc remove(nums: inout [Int], index: Int) {\n    // Move all elements after index index forward by one position\n    for i in nums.indices.dropFirst(index).dropLast() {\n        nums[i] = nums[i + 1]\n    }\n}\n
    array.js
    /* Remove the element at index index */\nfunction remove(nums, index) {\n    // Move all elements after index index forward by one position\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.ts
    /* Remove the element at index index */\nfunction remove(nums: number[], index: number): void {\n    // Move all elements after index index forward by one position\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.dart
    /* Remove the element at index index */\nvoid remove(List<int> nums, int index) {\n  // Move all elements after index index forward by one position\n  for (var i = index; i < nums.length - 1; i++) {\n    nums[i] = nums[i + 1];\n  }\n}\n
    array.rs
    /* Remove the element at index index */\nfn remove(nums: &mut [i32], index: usize) {\n    // Move all elements after index index forward by one position\n    for i in index..nums.len() - 1 {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.c
    /* Remove the element at index index */\n// Note: stdio.h occupies the remove keyword\nvoid removeItem(int *nums, int size, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
    array.kt
    /* Remove the element at index index */\nfun remove(nums: IntArray, index: Int) {\n    // Move all elements after index index forward by one position\n    for (i in index..<nums.size - 1) {\n        nums[i] = nums[i + 1]\n    }\n}\n
    array.rb
    ### Delete element at index ###\ndef remove(nums, index)\n  # Move all elements after index index forward by one position\n  for i in index...(nums.length - 1)\n    nums[i] = nums[i + 1]\n  end\nend\n

    Overall, array insertion and deletion operations have the following drawbacks:

    • High time complexity: The average time complexity for both insertion and deletion in arrays is \\(O(n)\\), where \\(n\\) is the length of the array.
    • Loss of elements: Since the length of an array is immutable, after inserting an element, elements that exceed the array's length will be lost.
    • Memory waste: We can initialize a relatively long array and use only the front portion, so that any overwritten tail elements are merely unused placeholders, but this wastes some memory space.
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5-traversing-arrays","level":3,"title":"5.   Traversing Arrays","text":"

    In most programming languages, we can traverse an array either by index or by directly iterating through each element in the array:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def traverse(nums: list[int]):\n    \"\"\"Traverse array\"\"\"\n    count = 0\n    # Traverse array by index\n    for i in range(len(nums)):\n        count += nums[i]\n    # Direct traversal of array elements\n    for num in nums:\n        count += num\n    # Traverse simultaneously data index and elements\n    for i, num in enumerate(nums):\n        count += nums[i]\n        count += num\n
    array.cpp
    /* Traverse array */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
    array.java
    /* Traverse array */\nvoid traverse(int[] nums) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (int num : nums) {\n        count += num;\n    }\n}\n
    array.cs
    /* Traverse array */\nvoid Traverse(int[] nums) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < nums.Length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    foreach (int num in nums) {\n        count += num;\n    }\n}\n
    array.go
    /* Traverse array */\nfunc traverse(nums []int) {\n    count := 0\n    // Traverse array by index\n    for i := 0; i < len(nums); i++ {\n        count += nums[i]\n    }\n    count = 0\n    // Direct traversal of array elements\n    for _, num := range nums {\n        count += num\n    }\n    // Traverse simultaneously data index and elements\n    for i, num := range nums {\n        count += nums[i]\n        count += num\n    }\n}\n
    array.swift
    /* Traverse array */\nfunc traverse(nums: [Int]) {\n    var count = 0\n    // Traverse array by index\n    for i in nums.indices {\n        count += nums[i]\n    }\n    // Direct traversal of array elements\n    for num in nums {\n        count += num\n    }\n    // Traverse simultaneously data index and elements\n    for (i, num) in nums.enumerated() {\n        count += nums[i]\n        count += num\n    }\n}\n
    array.js
    /* Traverse array */\nfunction traverse(nums) {\n    let count = 0;\n    // Traverse array by index\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (const num of nums) {\n        count += num;\n    }\n}\n
    array.ts
    /* Traverse array */\nfunction traverse(nums: number[]): void {\n    let count = 0;\n    // Traverse array by index\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (const num of nums) {\n        count += num;\n    }\n}\n
    array.dart
    /* Traverse array elements */\nvoid traverse(List<int> nums) {\n  int count = 0;\n  // Traverse array by index\n  for (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n  }\n  // Direct traversal of array elements\n  for (int _num in nums) {\n    count += _num;\n  }\n  // Traverse array using forEach method\n  nums.forEach((_num) {\n    count += _num;\n  });\n}\n
    array.rs
    /* Traverse array */\nfn traverse(nums: &[i32]) {\n    let mut _count = 0;\n    // Traverse array by index\n    for i in 0..nums.len() {\n        _count += nums[i];\n    }\n    // Direct traversal of array elements\n    _count = 0;\n    for &num in nums {\n        _count += num;\n    }\n}\n
    array.c
    /* Traverse array */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
    array.kt
    /* Traverse array */\nfun traverse(nums: IntArray) {\n    var count = 0\n    // Traverse array by index\n    for (i in nums.indices) {\n        count += nums[i]\n    }\n    // Direct traversal of array elements\n    for (j in nums) {\n        count += j\n    }\n}\n
    array.rb
    ### Traverse array ###\ndef traverse(nums)\n  count = 0\n\n  # Traverse array by index\n  for i in 0...nums.length\n    count += nums[i]\n  end\n\n  # Direct traversal of array elements\n  for num in nums\n    count += num\n  end\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6-finding-elements","level":3,"title":"6.   Finding Elements","text":"

    Finding a specified element in an array requires traversing the array and checking whether the element value matches in each iteration; if it matches, output the corresponding index.

    Since an array is a linear data structure, the above search operation is called a \"linear search\".

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def find(nums: list[int], target: int) -> int:\n    \"\"\"Find the specified element in the array\"\"\"\n    for i in range(len(nums)):\n        if nums[i] == target:\n            return i\n    return -1\n
    array.cpp
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    /* Find the specified element in the array */\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
    ### Find specified element in array ###\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":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7-expanding-arrays","level":3,"title":"7.   Expanding Arrays","text":"

    In complex system environments, programs cannot guarantee that the memory space after an array is available, making it unsafe to expand the array's capacity. Therefore, in most programming languages, the length of an array is immutable.

    If we want to expand an array, we need to create a new, larger array and then copy the original array elements to the new array one by one. This is an \\(O(n)\\) operation, which is very time-consuming when the array is large. The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def extend(nums: list[int], enlarge: int) -> list[int]:\n    \"\"\"Extend array length\"\"\"\n    # Initialize an array with extended length\n    res = [0] * (len(nums) + enlarge)\n    # Copy all elements from the original array to the new array\n    for i in range(len(nums)):\n        res[i] = nums[i]\n    # Return the extended new array\n    return res\n
    array.cpp
    /* Extend array length */\nint *extend(int *nums, int size, int enlarge) {\n    // Initialize an array with extended length\n    int *res = new int[size + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Free memory\n    delete[] nums;\n    // Return the extended new array\n    return res;\n}\n
    array.java
    /* Extend array length */\nint[] extend(int[] nums, int enlarge) {\n    // Initialize an array with extended length\n    int[] res = new int[nums.length + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.cs
    /* Extend array length */\nint[] Extend(int[] nums, int enlarge) {\n    // Initialize an array with extended length\n    int[] res = new int[nums.Length + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < nums.Length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.go
    /* Extend array length */\nfunc extend(nums []int, enlarge int) []int {\n    // Initialize an array with extended length\n    res := make([]int, len(nums)+enlarge)\n    // Copy all elements from the original array to the new array\n    for i, num := range nums {\n        res[i] = num\n    }\n    // Return the extended new array\n    return res\n}\n
    array.swift
    /* Extend array length */\nfunc extend(nums: [Int], enlarge: Int) -> [Int] {\n    // Initialize an array with extended length\n    var res = Array(repeating: 0, count: nums.count + enlarge)\n    // Copy all elements from the original array to the new array\n    for i in nums.indices {\n        res[i] = nums[i]\n    }\n    // Return the extended new array\n    return res\n}\n
    array.js
    /* Extend array length */\n// Note: JavaScript's Array is dynamic array, can be directly expanded\n// For learning purposes, this function treats Array as fixed-length array\nfunction extend(nums, enlarge) {\n    // Initialize an array with extended length\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Copy all elements from the original array to the new array\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.ts
    /* Extend array length */\n// Note: TypeScript's Array is dynamic array, can be directly expanded\n// For learning purposes, this function treats Array as fixed-length array\nfunction extend(nums: number[], enlarge: number): number[] {\n    // Initialize an array with extended length\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Copy all elements from the original array to the new array\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.dart
    /* Extend array length */\nList<int> extend(List<int> nums, int enlarge) {\n  // Initialize an array with extended length\n  List<int> res = List.filled(nums.length + enlarge, 0);\n  // Copy all elements from the original array to the new array\n  for (var i = 0; i < nums.length; i++) {\n    res[i] = nums[i];\n  }\n  // Return the extended new array\n  return res;\n}\n
    array.rs
    /* Extend array length */\nfn extend(nums: &[i32], enlarge: usize) -> Vec<i32> {\n    // Initialize an array with extended length\n    let mut res: Vec<i32> = vec![0; nums.len() + enlarge];\n    // Copy all elements from original array to new\n    res[0..nums.len()].copy_from_slice(nums);\n\n    // Return the extended new array\n    res\n}\n
    array.c
    /* Extend array length */\nint *extend(int *nums, int size, int enlarge) {\n    // Initialize an array with extended length\n    int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Initialize expanded space\n    for (int i = size; i < size + enlarge; i++) {\n        res[i] = 0;\n    }\n    // Return the extended new array\n    return res;\n}\n
    array.kt
    /* Extend array length */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n    // Initialize an array with extended length\n    val res = IntArray(nums.size + enlarge)\n    // Copy all elements from the original array to the new array\n    for (i in nums.indices) {\n        res[i] = nums[i]\n    }\n    // Return the extended new array\n    return res\n}\n
    array.rb
    ### Extend array length ###\n# Note: Ruby's Array is dynamic array, can be directly expanded\n# For learning purposes, this function treats Array as fixed-length array\ndef extend(nums, enlarge)\n  # Initialize an array with extended length\n  res = Array.new(nums.length + enlarge, 0)\n\n  # Copy all elements from the original array to the new array\n  for i in 0...nums.length\n    res[i] = nums[i]\n  end\n\n  # Return the extended new array\n  res\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412-advantages-and-limitations-of-arrays","level":2,"title":"4.1.2   Advantages and Limitations of Arrays","text":"

    Arrays are stored in contiguous memory space with elements of the same type. This approach contains rich prior information that the system can use to optimize the efficiency of data structure operations.

    • High space efficiency: Arrays allocate contiguous memory blocks for data without additional structural overhead.
    • Support for random access: Arrays allow accessing any element in \\(O(1)\\) time.
    • Cache locality: When accessing array elements, the computer not only loads the element but also caches the surrounding data, thereby leveraging the cache to improve the execution speed of subsequent operations.

    Contiguous space storage is a double-edged sword with the following limitations:

    • Low insertion and deletion efficiency: When an array has many elements, insertion and deletion operations require shifting a large number of elements.
    • Immutable length: After an array is initialized, its length is fixed. Expanding the array requires copying all data to a new array, which is very costly.
    • Space waste: If the allocated size of an array exceeds what is actually needed, the extra space is wasted.
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413-typical-applications-of-arrays","level":2,"title":"4.1.3   Typical Applications of Arrays","text":"

    Arrays are a fundamental and common data structure, frequently used in various algorithms and for implementing various complex data structures.

    • Random access: If we want to randomly sample some items, we can use an array to store them and generate a random sequence to implement random sampling based on indices.
    • Sorting and searching: Arrays are the most commonly used data structure for sorting and searching algorithms. Quick sort, merge sort, binary search, and others are primarily performed on arrays.
    • Lookup tables: When we need to quickly find an element or its corresponding relationship, we can use an array as a lookup table. For example, if we want to implement a mapping from characters to ASCII codes, we can use the ASCII code value of a character as an index, with the corresponding element stored at that position in the array.
    • Machine learning: Neural networks make extensive use of linear algebra operations between vectors, matrices, and tensors, all of which are constructed in the form of arrays. Arrays are the most commonly used data structure in neural network programming.
    • Data structure implementation: Arrays can be used to implement stacks, queues, hash tables, heaps, graphs, and other data structures. For example, the adjacency matrix representation of a graph is essentially a two-dimensional array.
    ","path":["Chapter 4. Arrays and Linked Lists","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   Linked List","text":"

    Memory is a shared resource for all programs. In a complex runtime environment, free memory may be scattered throughout the address space. We know that arrays require contiguous memory, and when an array is very large, the system may not be able to provide such a large contiguous block. This is where the flexibility of linked lists becomes apparent.

    A linked list is a linear data structure in which each element is a node object, and the nodes are connected through \"references\". A reference records the memory address of the next node, through which the next node can be accessed from the current node.

    This design allows linked-list nodes to be stored in different locations in memory, and their addresses do not need to be contiguous.

    Figure 4-5   Linked list definition and storage method

    Observing Figure 4-5, the basic unit of a linked list is a node object. Each node contains two pieces of data: the node's \"value\" and a \"reference\" to the next node.

    • The first node of a linked list is called the \"head node\", and the last node is called the \"tail node\".
    • The tail node points to \"null\", which is denoted as null, nullptr, and None in Java, C++, and Python, respectively.
    • In languages that support pointers, such as C, C++, Go, and Rust, the aforementioned \"reference\" should be replaced with \"pointer\".

    As shown in the following code, a linked list node ListNode contains not only a value but also an additional reference (pointer). Therefore, linked lists occupy more memory space than arrays when storing the same amount of data.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class ListNode:\n    \"\"\"Linked list node class\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val               # Node value\n        self.next: ListNode | None = None # Reference to the next node\n
    /* Linked list node structure */\nstruct ListNode {\n    int val;         // Node value\n    ListNode *next;  // Pointer to the next node\n    ListNode(int x) : val(x), next(nullptr) {}  // Constructor\n};\n
    /* Linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode next;  // Reference to the next node\n    ListNode(int x) { val = x; }  // Constructor\n}\n
    /* Linked list node class */\nclass ListNode(int x) {  // Constructor\n    int val = x;         // Node value\n    ListNode? next;      // Reference to the next node\n}\n
    /* Linked list node structure */\ntype ListNode struct {\n    Val  int       // Node value\n    Next *ListNode // Pointer to the next node\n}\n\n// NewListNode Constructor, creates a new linked list\nfunc NewListNode(val int) *ListNode {\n    return &ListNode{\n        Val:  val,\n        Next: nil,\n    }\n}\n
    /* Linked list node class */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Reference to the next node\n\n    init(x: Int) { // Constructor\n        val = x\n    }\n}\n
    /* Linked list node class */\nclass ListNode {\n    constructor(val, next) {\n        this.val = (val === undefined ? 0 : val);       // Node value\n        this.next = (next === undefined ? null : next); // Reference to the next node\n    }\n}\n
    /* Linked list node class */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    constructor(val?: number, next?: ListNode | null) {\n        this.val = val === undefined ? 0 : val;        // Node value\n        this.next = next === undefined ? null : next;  // Reference to the next node\n    }\n}\n
    /* Linked list node class */\nclass ListNode {\n  int val; // Node value\n  ListNode? next; // Reference to the next node\n  ListNode(this.val, [this.next]); // Constructor\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n/* Linked list node class */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Node value\n    next: Option<Rc<RefCell<ListNode>>>, // Pointer to the next node\n}\n
    /* Linked list node structure */\ntypedef struct ListNode {\n    int val;               // Node value\n    struct ListNode *next; // Pointer to the next node\n} ListNode;\n\n/* Constructor */\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
    /* Linked list node class */\n// Constructor\nclass ListNode(x: Int) {\n    val _val: Int = x          // Node value\n    val next: ListNode? = null // Reference to the next node\n}\n
    # Linked list node class\nclass ListNode\n  attr_accessor :val  # Node value\n  attr_accessor :next # Reference to the next node\n\n  def initialize(val=0, next_node=nil)\n    @val = val\n    @next = next_node\n  end\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421-common-linked-list-operations","level":2,"title":"4.2.1   Common Linked List Operations","text":"","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1-initializing-a-linked-list","level":3,"title":"1.   Initializing a Linked List","text":"

    Building a linked list involves two steps: first, initializing each node object; second, constructing the reference relationships between nodes. Once initialization is complete, we can traverse all nodes starting from the head node of the linked list through the reference next.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    # Initialize linked list 1 -> 3 -> 2 -> 5 -> 4\n# Initialize each node\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
    linked_list.cpp
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
    linked_list.java
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.cs
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.go
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// Build references between nodes\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n
    linked_list.swift
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
    linked_list.js
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.ts
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.dart
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\\\n// Initialize each node\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.rs
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\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// Build references between nodes\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
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// Build references between nodes\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
    linked_list.kt
    /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
    linked_list.rb
    # Initialize linked list 1 -> 3 -> 2 -> 5 -> 4\n# Initialize each node\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
    Code Visualization

    Full Screen >

    An array is a single variable; for example, an array nums contains elements nums[0], nums[1], and so on. A linked list, by contrast, is composed of multiple independent node objects. We usually use the head node as a stand-in for the entire linked list; for example, the linked list in the above code can be referred to as linked list n0.

    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2-inserting-a-node","level":3,"title":"2.   Inserting a Node","text":"

    Inserting a node in a linked list is very easy. As shown in Figure 4-6, suppose we want to insert a new node P between two adjacent nodes n0 and n1. We only need to change two node references (pointers), with a time complexity of \\(O(1)\\).

    In contrast, the time complexity of inserting an element in an array is \\(O(n)\\), which is inefficient when dealing with large amounts of data.

    Figure 4-6   Example of inserting a node into a linked list

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def insert(n0: ListNode, P: ListNode):\n    \"\"\"Insert node P after node n0 in the linked list\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
    linked_list.cpp
    /* Insert node P after node n0 in the linked list */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.java
    /* Insert node P after node n0 in the linked list */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.cs
    /* Insert node P after node n0 in the linked list */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.go
    /* Insert node P after node n0 in the linked list */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
    linked_list.swift
    /* Insert node P after node n0 in the linked list */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
    linked_list.js
    /* Insert node P after node n0 in the linked list */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.ts
    /* Insert node P after node n0 in the linked list */\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
    /* Insert node P after node n0 in the linked list */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
    linked_list.rs
    /* Insert node P after node n0 in the linked list */\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
    /* Insert node P after node n0 in the linked list */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.kt
    /* Insert node P after node n0 in the linked list */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
    linked_list.rb
    ### Insert node _p after node n0 in linked list ###\n# Ruby's `p` is a built-in function, `P` is a constant, so use `_p` instead\ndef insert(n0, _p)\n  n1 = n0.next\n  _p.next = n1\n  n0.next = _p\nend\n
    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3-removing-a-node","level":3,"title":"3.   Removing a Node","text":"

    As shown in Figure 4-7, removing a node in a linked list is also very convenient. We only need to change one node's reference (pointer).

    Note that although node P still points to n1 after the deletion operation is complete, the linked list can no longer access P when traversing, which means P no longer belongs to this linked list.

    Figure 4-7   Removing a node from a linked list

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def remove(n0: ListNode):\n    \"\"\"Remove the first node after node n0 in the linked list\"\"\"\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
    /* Remove the first node after node n0 in the linked list */\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    // Free memory\n    delete P;\n}\n
    linked_list.java
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\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
    /* Remove the first node after node n0 in the linked list */\n// Note: stdio.h occupies the remove keyword\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    // Free memory\n    free(P);\n}\n
    linked_list.kt
    /* Remove the first node after node n0 in the linked list */\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
    ### Delete first node after node n0 in linked list ###\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":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4-accessing-a-node","level":3,"title":"4.   Accessing a Node","text":"

    Accessing nodes in a linked list is less efficient. As mentioned in the previous section, we can access any element in an array in \\(O(1)\\) time. This is not the case with linked lists. The program needs to start from the head node and traverse backward one by one until the target node is found. That is, accessing the \\(i\\)-th node in a linked list requires \\(i - 1\\) iterations, with a time complexity of \\(O(n)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def access(head: ListNode, index: int) -> ListNode | None:\n    \"\"\"Access the node at index index in the linked list\"\"\"\n    for _ in range(index):\n        if not head:\n            return None\n        head = head.next\n    return head\n
    linked_list.cpp
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    /* Access the node at index index in the linked list */\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
    ### Access node at index in linked list ###\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":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5-finding-a-node","level":3,"title":"5.   Finding a Node","text":"

    Traverse the linked list to find a node with value target, and output the index of that node in the linked list. This process is also a linear search. The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def find(head: ListNode, target: int) -> int:\n    \"\"\"Find the first node with value target in the linked list\"\"\"\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    /* Find the first node with value target in the linked list */\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
    ### Find first node with value target in linked list ###\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":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422-arrays-vs-linked-lists","level":2,"title":"4.2.2   Arrays vs. Linked Lists","text":"

    Table 4-1 summarizes the characteristics of arrays and linked lists and compares their operational efficiencies. Since they employ two opposite storage strategies, their various properties and operational efficiencies also exhibit contrasting characteristics.

    Table 4-1   Comparison of array and linked list efficiencies

    Array Linked List Storage method Contiguous memory space Scattered memory space Capacity expansion Immutable length Flexible expansion Memory efficiency Elements occupy less memory, but space may be wasted Elements occupy more memory Accessing an element \\(O(1)\\) \\(O(n)\\) Adding an element \\(O(n)\\) \\(O(1)\\) Removing an element \\(O(n)\\) \\(O(1)\\)","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423-common-types-of-linked-lists","level":2,"title":"4.2.3   Common Types of Linked Lists","text":"

    As shown in Figure 4-8, there are three common types of linked lists:

    • Singly linked list: This is the ordinary linked list introduced earlier. The nodes of a singly linked list contain a value and a reference to the next node. We call the first node the head node and the last node the tail node; the tail node points to None.
    • Circular linked list: If we make the tail node of a singly linked list point to the head node (connecting the tail to the head), we get a circular linked list. In a circular linked list, any node can be viewed as the head node.
    • Doubly linked list: Compared to a singly linked list, a doubly linked list records references in both directions. The node definition of a doubly linked list includes references to both the successor node (next node) and the predecessor node (previous node). Compared to a singly linked list, a doubly linked list is more flexible and can traverse the linked list in both directions, but it also requires more memory space.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class ListNode:\n    \"\"\"Doubly linked list node class\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Node value\n        self.next: ListNode | None = None  # Reference to the successor node\n        self.prev: ListNode | None = None  # Reference to the predecessor node\n
    /* Doubly linked list node structure */\nstruct ListNode {\n    int val;         // Node value\n    ListNode *next;  // Pointer to the successor node\n    ListNode *prev;  // Pointer to the predecessor node\n    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}  // Constructor\n};\n
    /* Doubly linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode next;  // Reference to the successor node\n    ListNode prev;  // Reference to the predecessor node\n    ListNode(int x) { val = x; }  // Constructor\n}\n
    /* Doubly linked list node class */\nclass ListNode(int x) {  // Constructor\n    int val = x;    // Node value\n    ListNode next;  // Reference to the successor node\n    ListNode prev;  // Reference to the predecessor node\n}\n
    /* Doubly linked list node structure */\ntype DoublyListNode struct {\n    Val  int             // Node value\n    Next *DoublyListNode // Pointer to the successor node\n    Prev *DoublyListNode // Pointer to the predecessor node\n}\n\n// NewDoublyListNode Initialization\nfunc NewDoublyListNode(val int) *DoublyListNode {\n    return &DoublyListNode{\n        Val:  val,\n        Next: nil,\n        Prev: nil,\n    }\n}\n
    /* Doubly linked list node class */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Reference to the successor node\n    var prev: ListNode? // Reference to the predecessor node\n\n    init(x: Int) { // Constructor\n        val = x\n    }\n}\n
    /* Doubly linked list node class */\nclass ListNode {\n    constructor(val, next, prev) {\n        this.val = val  ===  undefined ? 0 : val;        // Node value\n        this.next = next  ===  undefined ? null : next;  // Reference to the successor node\n        this.prev = prev  ===  undefined ? null : prev;  // Reference to the predecessor node\n    }\n}\n
    /* Doubly linked list node class */\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;        // Node value\n        this.next = next  ===  undefined ? null : next;  // Reference to the successor node\n        this.prev = prev  ===  undefined ? null : prev;  // Reference to the predecessor node\n    }\n}\n
    /* Doubly linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode? next;  // Reference to the successor node\n    ListNode? prev;  // Reference to the predecessor node\n    ListNode(this.val, [this.next, this.prev]);  // Constructor\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Doubly linked list node type */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Node value\n    next: Option<Rc<RefCell<ListNode>>>, // Pointer to the successor node\n    prev: Option<Rc<RefCell<ListNode>>>, // Pointer to the predecessor node\n}\n\n/* Constructor */\nimpl ListNode {\n    fn new(val: i32) -> Self {\n        ListNode {\n            val,\n            next: None,\n            prev: None,\n        }\n    }\n}\n
    /* Doubly linked list node structure */\ntypedef struct ListNode {\n    int val;               // Node value\n    struct ListNode *next; // Pointer to the successor node\n    struct ListNode *prev; // Pointer to the predecessor node\n} ListNode;\n\n/* Constructor */\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
    /* Doubly linked list node class */\n// Constructor\nclass ListNode(x: Int) {\n    val _val: Int = x           // Node value\n    val next: ListNode? = null  // Reference to the successor node\n    val prev: ListNode? = null  // Reference to the predecessor node\n}\n
    # Doubly linked list node class\nclass ListNode\n  attr_accessor :val    # Node value\n  attr_accessor :next   # Reference to the successor node\n  attr_accessor :prev   # Reference to the predecessor node\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

    Figure 4-8   Common types of linked lists

    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424-typical-applications-of-linked-lists","level":2,"title":"4.2.4   Typical Applications of Linked Lists","text":"

    Singly linked lists are commonly used to implement stacks, queues, hash tables, and graphs.

    • Stacks and queues: When insertion and deletion operations both occur at one end of the linked list, it exhibits last-in-first-out characteristics, corresponding to a stack. When insertion operations occur at one end of the linked list and deletion operations occur at the other end, it exhibits first-in-first-out characteristics, corresponding to a queue.
    • Hash tables: Separate chaining is one of the mainstream solutions for resolving hash collisions. In this approach, all colliding elements are placed in a linked list.
    • Graphs: An adjacency list is a common way to represent a graph, where each vertex in the graph is associated with a linked list, and each element in the linked list represents another vertex connected to that vertex.

    Doubly linked lists are commonly used in scenarios where quick access to the previous and next elements is needed.

    • Advanced data structures: For example, in red-black trees and B-trees, we need to access the parent node of a node, which can be achieved by saving a reference to the parent node in the node, similar to a doubly linked list.
    • Browser history: In web browsers, when a user clicks the forward or backward button, the browser needs to know the previous and next web pages the user visited. The characteristics of doubly linked lists make this operation simple.
    • LRU algorithm: In cache eviction (LRU) algorithms, we need to quickly find the least recently used data and support quick addition and deletion of nodes. Using a doubly linked list is very suitable for this.

    Circular linked lists are commonly used in scenarios that require periodic operations, such as operating system resource scheduling.

    • Round-robin scheduling algorithm: In operating systems, round-robin scheduling is a common CPU scheduling algorithm that needs to cycle through a set of processes. Each process is assigned a time slice, and when the time slice expires, the CPU switches to the next process. This cyclic operation can be implemented using a circular linked list.
    • Data buffers: In some data buffer implementations, circular linked lists may also be used. For example, in audio and video players, the data stream may be divided into multiple buffer blocks and placed in a circular linked list to achieve seamless playback.
    ","path":["Chapter 4. Arrays and Linked Lists","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   List","text":"

    A list is an abstract data structure concept that represents an ordered collection of elements, supporting operations such as element access, modification, insertion, deletion, and traversal, without requiring users to consider capacity limitations. Lists can be implemented based on linked lists or arrays.

    • A linked list can naturally be viewed as a list: it supports insertion, deletion, search, and update, and can grow flexibly as needed.
    • An array also supports insertion, deletion, search, and update, but because its length is fixed, it can only be regarded as a list with a capacity limit.

    When a list is implemented with an array, its fixed length makes it less practical. This is because we usually cannot determine in advance how much data we need to store, making it difficult to choose an appropriate capacity. If the capacity is too small, it may fail to meet our needs; if it is too large, memory space will be wasted.

    To solve this problem, we can use a dynamic array to implement a list. It inherits all the advantages of arrays while supporting dynamic resizing during program execution.

    In fact, the list types provided by the standard libraries of many programming languages are implemented with dynamic arrays, such as list in Python, ArrayList in Java, vector in C++, and List in C#. In the following discussion, we will treat \"list\" and \"dynamic array\" as equivalent concepts.

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431-common-list-operations","level":2,"title":"4.3.1   Common List Operations","text":"","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1-initialize-a-list","level":3,"title":"1.   Initialize a List","text":"

    We typically initialize a list in one of two ways: empty or with predefined values:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Initialize a list\n# Without initial values\nnums1: list[int] = []\n# With initial values\nnums: list[int] = [1, 3, 2, 5, 4]\n
    list.cpp
    /* Initialize a list */\n// Note that vector in C++ is equivalent to nums as described in this article\n// Without initial values\nvector<int> nums1;\n// With initial values\nvector<int> nums = { 1, 3, 2, 5, 4 };\n
    list.java
    /* Initialize a list */\n// Without initial values\nList<Integer> nums1 = new ArrayList<>();\n// With initial values (note that array elements should use the wrapper class Integer[] instead of int[])\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n
    list.cs
    /* Initialize a list */\n// Without initial values\nList<int> nums1 = [];\n// With initial values\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n
    list_test.go
    /* Initialize a list */\n// Without initial values\nnums1 := []int{}\n// With initial values\nnums := []int{1, 3, 2, 5, 4}\n
    list.swift
    /* Initialize a list */\n// Without initial values\nlet nums1: [Int] = []\n// With initial values\nvar nums = [1, 3, 2, 5, 4]\n
    list.js
    /* Initialize a list */\n// Without initial values\nconst nums1 = [];\n// With initial values\nconst nums = [1, 3, 2, 5, 4];\n
    list.ts
    /* Initialize a list */\n// Without initial values\nconst nums1: number[] = [];\n// With initial values\nconst nums: number[] = [1, 3, 2, 5, 4];\n
    list.dart
    /* Initialize a list */\n// Without initial values\nList<int> nums1 = [];\n// With initial values\nList<int> nums = [1, 3, 2, 5, 4];\n
    list.rs
    /* Initialize a list */\n// Without initial values\nlet nums1: Vec<i32> = Vec::new();\n// With initial values\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Initialize a list */\n// Without initial values\nvar nums1 = listOf<Int>()\n// With initial values\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n
    list.rb
    # Initialize a list\n# Without initial values\nnums1 = []\n# With initial values\nnums = [1, 3, 2, 5, 4]\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2-access-elements","level":3,"title":"2.   Access Elements","text":"

    Since a list is essentially an array, we can access and update elements in \\(O(1)\\) time complexity, which is very efficient.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Access an element\nnum: int = nums[1]  # Access element at index 1\n\n# Update an element\nnums[1] = 0    # Update element at index 1 to 0\n
    list.cpp
    /* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list.java
    /* Access an element */\nint num = nums.get(1);  // Access element at index 1\n\n/* Update an element */\nnums.set(1, 0);  // Update element at index 1 to 0\n
    list.cs
    /* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list_test.go
    /* Access an element */\nnum := nums[1]  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0     // Update element at index 1 to 0\n
    list.swift
    /* Access an element */\nlet num = nums[1] // Access element at index 1\n\n/* Update an element */\nnums[1] = 0 // Update element at index 1 to 0\n
    list.js
    /* Access an element */\nconst num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list.ts
    /* Access an element */\nconst num: number = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list.dart
    /* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
    list.rs
    /* Access an element */\nlet num: i32 = nums[1];  // Access element at index 1\n/* Update an element */\nnums[1] = 0;             // Update element at index 1 to 0\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Access an element */\nval num = nums[1]       // Access element at index 1\n/* Update an element */\nnums[1] = 0             // Update element at index 1 to 0\n
    list.rb
    # Access an element\nnum = nums[1] # Access element at index 1\n# Update an element\nnums[1] = 0 # Update element at index 1 to 0\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3-insert-and-delete-elements","level":3,"title":"3.   Insert and Delete Elements","text":"

    Compared to arrays, lists can freely add and delete elements. Adding an element at the end of a list has a time complexity of \\(O(1)\\), but inserting and deleting elements still have the same efficiency as arrays, with a time complexity of \\(O(n)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Clear the list\nnums.clear()\n\n# Add elements at the end\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# Insert an element in the middle\nnums.insert(3, 6)  # Insert number 6 at index 3\n\n# Delete an element\nnums.pop(3)        # Delete element at index 3\n
    list.cpp
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* Insert an element in the middle */\nnums.insert(nums.begin() + 3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.erase(nums.begin() + 3);      // Delete element at index 3\n
    list.java
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.add(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);  // Delete element at index 3\n
    list.cs
    /* Clear the list */\nnums.Clear();\n\n/* Add elements at the end */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* Insert an element in the middle */\nnums.Insert(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.RemoveAt(3);  // Delete element at index 3\n
    list_test.go
    /* Clear the list */\nnums = nil\n\n/* Add elements at the end */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* Insert an element in the middle */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // Insert number 6 at index 3\n\n/* Delete an element */\nnums = append(nums[:3], nums[4:]...) // Delete element at index 3\n
    list.swift
    /* Clear the list */\nnums.removeAll()\n\n/* Add elements at the end */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* Insert an element in the middle */\nnums.insert(6, at: 3) // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(at: 3) // Delete element at index 3\n
    list.js
    /* Clear the list */\nnums.length = 0;\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.splice(3, 0, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.splice(3, 1);  // Delete element at index 3\n
    list.ts
    /* Clear the list */\nnums.length = 0;\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.splice(3, 0, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.splice(3, 1);  // Delete element at index 3\n
    list.dart
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.insert(3, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.removeAt(3); // Delete element at index 3\n
    list.rs
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.insert(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);    // Delete element at index 3\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.add(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);  // Delete element at index 3\n
    list.rb
    # Clear the list\nnums.clear\n\n# Add elements at the end\nnums << 1\nnums << 3\nnums << 2\nnums << 5\nnums << 4\n\n# Insert an element in the middle\nnums.insert(3, 6) # Insert number 6 at index 3\n\n# Delete an element\nnums.delete_at(3) # Delete element at index 3\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4-traverse-a-list","level":3,"title":"4.   Traverse a List","text":"

    Like arrays, lists can be traversed by index or by directly iterating through elements.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Traverse the list by index\ncount = 0\nfor i in range(len(nums)):\n    count += nums[i]\n\n# Traverse list elements directly\nfor num in nums:\n    count += num\n
    list.cpp
    /* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (int num : nums) {\n    count += num;\n}\n
    list.java
    /* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums.get(i);\n}\n\n/* Traverse list elements directly */\nfor (int num : nums) {\n    count += num;\n}\n
    list.cs
    /* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nforeach (int num in nums) {\n    count += num;\n}\n
    list_test.go
    /* Traverse the list by index */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\ncount = 0\nfor _, num := range nums {\n    count += num\n}\n
    list.swift
    /* Traverse the list by index */\nvar count = 0\nfor i in nums.indices {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\ncount = 0\nfor num in nums {\n    count += num\n}\n
    list.js
    /* Traverse the list by index */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
    list.ts
    /* Traverse the list by index */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
    list.dart
    /* Traverse the list by index */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (var num in nums) {\n    count += num;\n}\n
    list.rs
    // Traverse the list by index\nlet mut _count = 0;\nfor i in 0..nums.len() {\n    _count += nums[i];\n}\n\n// Traverse list elements directly\n_count = 0;\nfor num in &nums {\n    _count += num;\n}\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Traverse the list by index */\nvar count = 0\nfor (i in nums.indices) {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\nfor (num in nums) {\n    count += num\n}\n
    list.rb
    # Traverse the list by index\ncount = 0\nfor i in 0...nums.length\n    count += nums[i]\nend\n\n# Traverse list elements directly\ncount = 0\nfor num in nums\n    count += num\nend\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5-concatenate-lists","level":3,"title":"5.   Concatenate Lists","text":"

    Given a new list nums1, we can concatenate it to the end of the original list.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Concatenate two lists\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # Concatenate list nums1 to the end of nums\n
    list.cpp
    /* Concatenate two lists */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// Concatenate list nums1 to the end of nums\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
    list.java
    /* Concatenate two lists */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // Concatenate list nums1 to the end of nums\n
    list.cs
    /* Concatenate two lists */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // Concatenate list nums1 to the end of nums\n
    list_test.go
    /* Concatenate two lists */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // Concatenate list nums1 to the end of nums\n
    list.swift
    /* Concatenate two lists */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // Concatenate list nums1 to the end of nums\n
    list.js
    /* Concatenate two lists */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Concatenate list nums1 to the end of nums\n
    list.ts
    /* Concatenate two lists */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Concatenate list nums1 to the end of nums\n
    list.dart
    /* Concatenate two lists */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // Concatenate list nums1 to the end of nums\n
    list.rs
    /* Concatenate two lists */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Concatenate two lists */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // Concatenate list nums1 to the end of nums\n
    list.rb
    # Concatenate two lists\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6-sort-a-list","level":3,"title":"6.   Sort a List","text":"

    After sorting a list, we can use \"binary search\" and \"two-pointer\" algorithms, which are frequently tested in array algorithm problems.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
    # Sort a list\nnums.sort()  # After sorting, list elements are arranged from smallest to largest\n
    list.cpp
    /* Sort a list */\nsort(nums.begin(), nums.end());  // After sorting, list elements are arranged from smallest to largest\n
    list.java
    /* Sort a list */\nCollections.sort(nums);  // After sorting, list elements are arranged from smallest to largest\n
    list.cs
    /* Sort a list */\nnums.Sort(); // After sorting, list elements are arranged from smallest to largest\n
    list_test.go
    /* Sort a list */\nsort.Ints(nums)  // After sorting, list elements are arranged from smallest to largest\n
    list.swift
    /* Sort a list */\nnums.sort() // After sorting, list elements are arranged from smallest to largest\n
    list.js
    /* Sort a list */\nnums.sort((a, b) => a - b);  // After sorting, list elements are arranged from smallest to largest\n
    list.ts
    /* Sort a list */\nnums.sort((a, b) => a - b);  // After sorting, list elements are arranged from smallest to largest\n
    list.dart
    /* Sort a list */\nnums.sort(); // After sorting, list elements are arranged from smallest to largest\n
    list.rs
    /* Sort a list */\nnums.sort(); // After sorting, list elements are arranged from smallest to largest\n
    list.c
    // C does not provide built-in dynamic arrays\n
    list.kt
    /* Sort a list */\nnums.sort() // After sorting, list elements are arranged from smallest to largest\n
    list.rb
    # Sort a list\nnums = nums.sort { |a, b| a <=> b } # After sorting, list elements are arranged from smallest to largest\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432-list-implementation","level":2,"title":"4.3.2   List Implementation","text":"

    Many programming languages have built-in lists, such as Java, C++, and Python. Their implementations are quite complex, and the parameters are carefully considered, such as initial capacity, expansion multiples, and so on. Interested readers can consult the source code to learn more.

    To deepen our understanding of how lists work, we attempt to implement a simple list with three key design considerations:

    • Initial capacity: Select a reasonable initial capacity for the underlying array. In this example, we choose 10 as the initial capacity.
    • Size tracking: Declare a variable size to record the current number of elements in the list and update it in real-time as elements are inserted and deleted. Based on this variable, we can locate the end of the list and determine whether expansion is needed.
    • Expansion mechanism: When the list capacity is full upon inserting an element, we need to expand. We create a larger array based on the expansion multiple and then move all elements from the current array to the new array in order. In this example, we specify that the array should be expanded to 2 times its previous size each time.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py
    class MyList:\n    \"\"\"List class\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._capacity: int = 10  # List capacity\n        self._arr: list[int] = [0] * self._capacity  # Array (stores list elements)\n        self._size: int = 0  # List length (current number of elements)\n        self._extend_ratio: int = 2  # Multiple by which the list capacity is extended each time\n\n    def size(self) -> int:\n        \"\"\"Get list length (current number of elements)\"\"\"\n        return self._size\n\n    def capacity(self) -> int:\n        \"\"\"Get list capacity\"\"\"\n        return self._capacity\n\n    def get(self, index: int) -> int:\n        \"\"\"Access element\"\"\"\n        # If the index is out of bounds, throw an exception, as below\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        return self._arr[index]\n\n    def set(self, num: int, index: int):\n        \"\"\"Update element\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        self._arr[index] = num\n\n    def add(self, num: int):\n        \"\"\"Add element at the end\"\"\"\n        # When the number of elements exceeds capacity, trigger the extension mechanism\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        \"\"\"Insert element in the middle\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        # When the number of elements exceeds capacity, trigger the extension mechanism\n        if self._size == self.capacity():\n            self.extend_capacity()\n        # Move all elements at and after index index backward by one position\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        # Update the number of elements\n        self._size += 1\n\n    def remove(self, index: int) -> int:\n        \"\"\"Remove element\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        num = self._arr[index]\n        # Move all elements after index index forward by one position\n        for j in range(index, self._size - 1):\n            self._arr[j] = self._arr[j + 1]\n        # Update the number of elements\n        self._size -= 1\n        # Return the removed element\n        return num\n\n    def extend_capacity(self):\n        \"\"\"Extend list capacity\"\"\"\n        # Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n        # Update list capacity\n        self._capacity = len(self._arr)\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return list with valid length\"\"\"\n        return self._arr[: self._size]\n
    my_list.cpp
    /* List class */\nclass MyList {\n  private:\n    int *arr;             // Array (stores list elements)\n    int arrCapacity = 10; // List capacity\n    int arrSize = 0;      // List length (current number of elements)\n    int extendRatio = 2;   // Multiple by which the list capacity is extended each time\n\n  public:\n    /* Constructor */\n    MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Destructor */\n    ~MyList() {\n        delete[] arr;\n    }\n\n    /* Get list length (current number of elements)*/\n    int size() {\n        return arrSize;\n    }\n\n    /* Get list capacity */\n    int capacity() {\n        return arrCapacity;\n    }\n\n    /* Update element */\n    int get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    void set(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    void add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size() == capacity())\n            extendCapacity();\n        arr[size()] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Sort list */\n    void insert(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size() == capacity())\n            extendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = size() - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Remove element */\n    int remove(int index) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        int num = arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for (int j = index; j < size() - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        arrSize--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    void extendCapacity() {\n        // Create a new array with length extendRatio times the original array\n        int newCapacity = capacity() * extendRatio;\n        int *tmp = arr;\n        arr = new int[newCapacity];\n        // Copy all elements from the original array to the new array\n        for (int i = 0; i < size(); i++) {\n            arr[i] = tmp[i];\n        }\n        // Free memory\n        delete[] tmp;\n        arrCapacity = newCapacity;\n    }\n\n    /* Convert list to Vector for printing */\n    vector<int> toVector() {\n        // Elements enqueue\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
    /* List class */\nclass MyList {\n    private int[] arr; // Array (stores list elements)\n    private int capacity = 10; // List capacity\n    private int size = 0; // List length (current number of elements)\n    private int extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    public MyList() {\n        arr = new int[capacity];\n    }\n\n    /* Get list length (current number of elements) */\n    public int size() {\n        return size;\n    }\n\n    /* Get list capacity */\n    public int capacity() {\n        return capacity;\n    }\n\n    /* Update element */\n    public int get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    public void set(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public void add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity();\n        arr[size] = num;\n        // Update the number of elements\n        size++;\n    }\n\n    /* Sort list */\n    public void insert(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = size - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        size++;\n    }\n\n    /* Remove element */\n    public int remove(int index) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        int num = arr[index];\n        // Move all elements after index forward by one position\n        for (int j = index; j < size - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public void extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = Arrays.copyOf(arr, capacity() * extendRatio);\n        // Add elements at the end\n        capacity = arr.length;\n    }\n\n    /* Convert list to array */\n    public int[] toArray() {\n        int size = size();\n        // Elements enqueue\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
    /* List class */\nclass MyList {\n    private int[] arr;           // Array (stores list elements)\n    private int arrCapacity = 10;    // List capacity\n    private int arrSize = 0;         // List length (current number of elements)\n    private readonly int extendRatio = 2;  // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    public MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Get list length (current number of elements) */\n    public int Size() {\n        return arrSize;\n    }\n\n    /* Get list capacity */\n    public int Capacity() {\n        return arrCapacity;\n    }\n\n    /* Update element */\n    public int Get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    public void Set(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public void Add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        arr[arrSize] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Sort list */\n    public void Insert(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = arrSize - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Remove element */\n    public int Remove(int index) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        int num = arr[index];\n        // Move all elements after index forward by one position\n        for (int j = index; j < arrSize - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        arrSize--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public void ExtendCapacity() {\n        // Create new array of length arrCapacity * extendRatio and copy original array to new array\n        Array.Resize(ref arr, arrCapacity * extendRatio);\n        // Add elements at the end\n        arrCapacity = arr.Length;\n    }\n\n    /* Convert list to array */\n    public int[] ToArray() {\n        // Elements enqueue\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
    /* List class */\ntype myList struct {\n    arrCapacity int\n    arr         []int\n    arrSize     int\n    extendRatio int\n}\n\n/* Constructor */\nfunc newMyList() *myList {\n    return &myList{\n        arrCapacity: 10,              // List capacity\n        arr:         make([]int, 10), // Array (stores list elements)\n        arrSize:     0,               // List length (current number of elements)\n        extendRatio: 2,               // Multiple by which the list capacity is extended each time\n    }\n}\n\n/* Get list length (current number of elements) */\nfunc (l *myList) size() int {\n    return l.arrSize\n}\n\n/* Get list capacity */\nfunc (l *myList) capacity() int {\n    return l.arrCapacity\n}\n\n/* Update element */\nfunc (l *myList) get(index int) int {\n    // If the index is out of bounds, throw an exception, as below\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    return l.arr[index]\n}\n\n/* Add elements at the end */\nfunc (l *myList) set(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    l.arr[index] = num\n}\n\n/* Direct traversal of list elements */\nfunc (l *myList) add(num int) {\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    l.arr[l.arrSize] = num\n    // Update the number of elements\n    l.arrSize++\n}\n\n/* Sort list */\nfunc (l *myList) insert(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    // Move all elements after index index forward by one position\n    for j := l.arrSize - 1; j >= index; j-- {\n        l.arr[j+1] = l.arr[j]\n    }\n    l.arr[index] = num\n    // Update the number of elements\n    l.arrSize++\n}\n\n/* Remove element */\nfunc (l *myList) remove(index int) int {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    num := l.arr[index]\n    // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n    for j := index; j < l.arrSize-1; j++ {\n        l.arr[j] = l.arr[j+1]\n    }\n    // Update the number of elements\n    l.arrSize--\n    // Return the removed element\n    return num\n}\n\n/* Driver Code */\nfunc (l *myList) extendCapacity() {\n    // Create a new array with length extendRatio times the original array and copy the original array to the new array\n    l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n    // Add elements at the end\n    l.arrCapacity = len(l.arr)\n}\n\n/* Return list with valid length */\nfunc (l *myList) toArray() []int {\n    // Elements enqueue\n    return l.arr[:l.arrSize]\n}\n
    my_list.swift
    /* List class */\nclass MyList {\n    private var arr: [Int] // Array (stores list elements)\n    private var _capacity: Int // List capacity\n    private var _size: Int // List length (current number of elements)\n    private let extendRatio: Int // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    init() {\n        _capacity = 10\n        _size = 0\n        extendRatio = 2\n        arr = Array(repeating: 0, count: _capacity)\n    }\n\n    /* Get list length (current number of elements) */\n    func size() -> Int {\n        _size\n    }\n\n    /* Get list capacity */\n    func capacity() -> Int {\n        _capacity\n    }\n\n    /* Update element */\n    func get(index: Int) -> Int {\n        // Throw error if index out of bounds, same below\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        return arr[index]\n    }\n\n    /* Add elements at the end */\n    func set(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        arr[index] = num\n    }\n\n    /* Direct traversal of list elements */\n    func add(num: Int) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if size() == capacity() {\n            extendCapacity()\n        }\n        arr[size()] = num\n        // Update the number of elements\n        _size += 1\n    }\n\n    /* Sort list */\n    func insert(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if size() == capacity() {\n            extendCapacity()\n        }\n        // Move all elements after index index forward by one position\n        for j in (index ..< size()).reversed() {\n            arr[j + 1] = arr[j]\n        }\n        arr[index] = num\n        // Update the number of elements\n        _size += 1\n    }\n\n    /* Remove element */\n    @discardableResult\n    func remove(index: Int) -> Int {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        let num = arr[index]\n        // Move all elements after index forward by one position\n        for j in index ..< (size() - 1) {\n            arr[j] = arr[j + 1]\n        }\n        // Update the number of elements\n        _size -= 1\n        // Return the removed element\n        return num\n    }\n\n    /* Driver Code */\n    func extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n        // Add elements at the end\n        _capacity = arr.count\n    }\n\n    /* Convert list to array */\n    func toArray() -> [Int] {\n        Array(arr.prefix(size()))\n    }\n}\n
    my_list.js
    /* List class */\nclass MyList {\n    #arr = new Array(); // Array (stores list elements)\n    #capacity = 10; // List capacity\n    #size = 0; // List length (current number of elements)\n    #extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    constructor() {\n        this.#arr = new Array(this.#capacity);\n    }\n\n    /* Get list length (current number of elements) */\n    size() {\n        return this.#size;\n    }\n\n    /* Get list capacity */\n    capacity() {\n        return this.#capacity;\n    }\n\n    /* Update element */\n    get(index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        return this.#arr[index];\n    }\n\n    /* Add elements at the end */\n    set(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        this.#arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    add(num) {\n        // If length equals capacity, need to expand\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Add new element to end of list\n        this.#arr[this.#size] = num;\n        this.#size++;\n    }\n\n    /* Sort list */\n    insert(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Move all elements after index index forward by one position\n        for (let j = this.#size - 1; j >= index; j--) {\n            this.#arr[j + 1] = this.#arr[j];\n        }\n        // Update the number of elements\n        this.#arr[index] = num;\n        this.#size++;\n    }\n\n    /* Remove element */\n    remove(index) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        let num = this.#arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for (let j = index; j < this.#size - 1; j++) {\n            this.#arr[j] = this.#arr[j + 1];\n        }\n        // Update the number of elements\n        this.#size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        this.#arr = this.#arr.concat(\n            new Array(this.capacity() * (this.#extendRatio - 1))\n        );\n        // Add elements at the end\n        this.#capacity = this.#arr.length;\n    }\n\n    /* Convert list to array */\n    toArray() {\n        let size = this.size();\n        // Elements enqueue\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
    /* List class */\nclass MyList {\n    private arr: Array<number>; // Array (stores list elements)\n    private _capacity: number = 10; // List capacity\n    private _size: number = 0; // List length (current number of elements)\n    private extendRatio: number = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    constructor() {\n        this.arr = new Array(this._capacity);\n    }\n\n    /* Get list length (current number of elements) */\n    public size(): number {\n        return this._size;\n    }\n\n    /* Get list capacity */\n    public capacity(): number {\n        return this._capacity;\n    }\n\n    /* Update element */\n    public get(index: number): number {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        return this.arr[index];\n    }\n\n    /* Add elements at the end */\n    public set(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        this.arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public add(num: number): void {\n        // If length equals capacity, need to expand\n        if (this._size === this._capacity) this.extendCapacity();\n        // Add new element to end of list\n        this.arr[this._size] = num;\n        this._size++;\n    }\n\n    /* Sort list */\n    public insert(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (this._size === this._capacity) {\n            this.extendCapacity();\n        }\n        // Move all elements after index index forward by one position\n        for (let j = this._size - 1; j >= index; j--) {\n            this.arr[j + 1] = this.arr[j];\n        }\n        // Update the number of elements\n        this.arr[index] = num;\n        this._size++;\n    }\n\n    /* Remove element */\n    public remove(index: number): number {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        let num = this.arr[index];\n        // Move all elements after index forward by one position\n        for (let j = index; j < this._size - 1; j++) {\n            this.arr[j] = this.arr[j + 1];\n        }\n        // Update the number of elements\n        this._size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public extendCapacity(): void {\n        // Create new array of length size and copy original array to new array\n        this.arr = this.arr.concat(\n            new Array(this.capacity() * (this.extendRatio - 1))\n        );\n        // Add elements at the end\n        this._capacity = this.arr.length;\n    }\n\n    /* Convert list to array */\n    public toArray(): number[] {\n        let size = this.size();\n        // Elements enqueue\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
    /* List class */\nclass MyList {\n  late List<int> _arr; // Array (stores list elements)\n  int _capacity = 10; // List capacity\n  int _size = 0; // List length (current number of elements)\n  int _extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n  /* Constructor */\n  MyList() {\n    _arr = List.filled(_capacity, 0);\n  }\n\n  /* Get list length (current number of elements) */\n  int size() => _size;\n\n  /* Get list capacity */\n  int capacity() => _capacity;\n\n  /* Update element */\n  int get(int index) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    return _arr[index];\n  }\n\n  /* Add elements at the end */\n  void set(int index, int _num) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    _arr[index] = _num;\n  }\n\n  /* Direct traversal of list elements */\n  void add(int _num) {\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (_size == _capacity) extendCapacity();\n    _arr[_size] = _num;\n    // Update the number of elements\n    _size++;\n  }\n\n  /* Sort list */\n  void insert(int index, int _num) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (_size == _capacity) extendCapacity();\n    // Move all elements after index index forward by one position\n    for (var j = _size - 1; j >= index; j--) {\n      _arr[j + 1] = _arr[j];\n    }\n    _arr[index] = _num;\n    // Update the number of elements\n    _size++;\n  }\n\n  /* Remove element */\n  int remove(int index) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    int _num = _arr[index];\n    // Move all elements after index forward by one position\n    for (var j = index; j < _size - 1; j++) {\n      _arr[j] = _arr[j + 1];\n    }\n    // Update the number of elements\n    _size--;\n    // Return the removed element\n    return _num;\n  }\n\n  /* Driver Code */\n  void extendCapacity() {\n    // Create new array with length _extendRatio times original array\n    final _newNums = List.filled(_capacity * _extendRatio, 0);\n    // Copy original array to new array\n    List.copyRange(_newNums, 0, _arr);\n    // Update _arr reference\n    _arr = _newNums;\n    // Add elements at the end\n    _capacity = _arr.length;\n  }\n\n  /* Convert list to array */\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
    /* List class */\n#[allow(dead_code)]\nstruct MyList {\n    arr: Vec<i32>,       // Array (stores list elements)\n    capacity: usize,     // List capacity\n    size: usize,         // List length (current number of elements)\n    extend_ratio: usize, // Multiple by which the list capacity is extended each time\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n    /* Constructor */\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    /* Get list length (current number of elements) */\n    pub fn size(&self) -> usize {\n        return self.size;\n    }\n\n    /* Get list capacity */\n    pub fn capacity(&self) -> usize {\n        return self.capacity;\n    }\n\n    /* Update element */\n    pub fn get(&self, index: usize) -> i32 {\n        // If the index is out of bounds, throw an exception, as below\n        if index >= self.size {\n            panic!(\"Index out of bounds\")\n        };\n        return self.arr[index];\n    }\n\n    /* Add elements at the end */\n    pub fn set(&mut self, index: usize, num: i32) {\n        if index >= self.size {\n            panic!(\"Index out of bounds\")\n        };\n        self.arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    pub fn add(&mut self, num: i32) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        self.arr[self.size] = num;\n        // Update the number of elements\n        self.size += 1;\n    }\n\n    /* Sort list */\n    pub fn insert(&mut self, index: usize, num: i32) {\n        if index >= self.size() {\n            panic!(\"Index out of bounds\")\n        };\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        // Move all elements after index index forward by one position\n        for j in (index..self.size).rev() {\n            self.arr[j + 1] = self.arr[j];\n        }\n        self.arr[index] = num;\n        // Update the number of elements\n        self.size += 1;\n    }\n\n    /* Remove element */\n    pub fn remove(&mut self, index: usize) -> i32 {\n        if index >= self.size() {\n            panic!(\"Index out of bounds\")\n        };\n        let num = self.arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for j in index..self.size - 1 {\n            self.arr[j] = self.arr[j + 1];\n        }\n        // Update the number of elements\n        self.size -= 1;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    pub fn extend_capacity(&mut self) {\n        // Create new array with length extend_ratio times original, copy original array to new array\n        let new_capacity = self.capacity * self.extend_ratio;\n        self.arr.resize(new_capacity, 0);\n        // Add elements at the end\n        self.capacity = new_capacity;\n    }\n\n    /* Convert list to array */\n    pub fn to_array(&self) -> Vec<i32> {\n        // Elements enqueue\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
    /* List class */\ntypedef struct {\n    int *arr;        // Array (stores list elements)\n    int capacity;    // List capacity\n    int size;        // List size\n    int extendRatio; // List expansion multiplier\n} MyList;\n\n/* Constructor */\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/* Destructor */\nvoid delMyList(MyList *nums) {\n    free(nums->arr);\n    free(nums);\n}\n\n/* Get list length */\nint size(MyList *nums) {\n    return nums->size;\n}\n\n/* Get list capacity */\nint capacity(MyList *nums) {\n    return nums->capacity;\n}\n\n/* Update element */\nint get(MyList *nums, int index) {\n    assert(index >= 0 && index < nums->size);\n    return nums->arr[index];\n}\n\n/* Add elements at the end */\nvoid set(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < nums->size);\n    nums->arr[index] = num;\n}\n\n/* Direct traversal of list elements */\nvoid add(MyList *nums, int num) {\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Expand capacity\n    }\n    nums->arr[size(nums)] = num;\n    nums->size++;\n}\n\n/* Sort list */\nvoid insert(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < size(nums));\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Expand capacity\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/* Remove element */\n// Note: stdio.h occupies the remove keyword\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/* Driver Code */\nvoid extendCapacity(MyList *nums) {\n    // Allocate space first\n    int newCapacity = capacity(nums) * nums->extendRatio;\n    int *extend = (int *)malloc(sizeof(int) * newCapacity);\n    int *temp = nums->arr;\n\n    // Copy old data to new data\n    for (int i = 0; i < size(nums); i++)\n        extend[i] = nums->arr[i];\n\n    // Free old data\n    free(temp);\n\n    // Update new data\n    nums->arr = extend;\n    nums->capacity = newCapacity;\n}\n\n/* Convert list to Array for printing */\nint *toArray(MyList *nums) {\n    return nums->arr;\n}\n
    my_list.kt
    /* List class */\nclass MyList {\n    private var arr: IntArray = intArrayOf() // Array (stores list elements)\n    private var capacity: Int = 10 // List capacity\n    private var size: Int = 0 // List length (current number of elements)\n    private var extendRatio: Int = 2 // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    init {\n        arr = IntArray(capacity)\n    }\n\n    /* Get list length (current number of elements) */\n    fun size(): Int {\n        return size\n    }\n\n    /* Get list capacity */\n    fun capacity(): Int {\n        return capacity\n    }\n\n    /* Update element */\n    fun get(index: Int): Int {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        return arr[index]\n    }\n\n    /* Add elements at the end */\n    fun set(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        arr[index] = num\n    }\n\n    /* Direct traversal of list elements */\n    fun add(num: Int) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity()\n        arr[size] = num\n        // Update the number of elements\n        size++\n    }\n\n    /* Sort list */\n    fun insert(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity()\n        // Move all elements after index index forward by one position\n        for (j in size - 1 downTo index)\n            arr[j + 1] = arr[j]\n        arr[index] = num\n        // Update the number of elements\n        size++\n    }\n\n    /* Remove element */\n    fun remove(index: Int): Int {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        val num = arr[index]\n        // Move all elements after index forward by one position\n        for (j in index..<size - 1)\n            arr[j] = arr[j + 1]\n        // Update the number of elements\n        size--\n        // Return the removed element\n        return num\n    }\n\n    /* Driver Code */\n    fun extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = arr.copyOf(capacity() * extendRatio)\n        // Add elements at the end\n        capacity = arr.size\n    }\n\n    /* Convert list to array */\n    fun toArray(): IntArray {\n        val size = size()\n        // Elements enqueue\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
    ### List class ###\nclass MyList\n  attr_reader :size       # Get list length (current number of elements)\n  attr_reader :capacity   # Get list capacity\n\n  ### Constructor ###\n  def initialize\n    @capacity = 10\n    @size = 0\n    @extend_ratio = 2\n    @arr = Array.new(capacity)\n  end\n\n  ### Access element ###\n  def get(index)\n    # If the index is out of bounds, throw an exception, as below\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    @arr[index]\n  end\n\n  ### Access element ###\n  def set(index, num)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    @arr[index] = num\n  end\n\n  ### Add element at end ###\n  def add(num)\n    # When the number of elements exceeds capacity, trigger the extension mechanism\n    extend_capacity if size == capacity\n    @arr[size] = num\n\n    # Update the number of elements\n    @size += 1\n  end\n\n  ### Insert element in middle ###\n  def insert(index, num)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n\n    # When the number of elements exceeds capacity, trigger the extension mechanism\n    extend_capacity if size == capacity\n\n    # Move all elements after index index forward by one position\n    for j in (size - 1).downto(index)\n      @arr[j + 1] = @arr[j]\n    end\n    @arr[index] = num\n\n    # Update the number of elements\n    @size += 1\n  end\n\n  ### Delete element ###\n  def remove(index)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    num = @arr[index]\n\n    # Move all elements after index forward by one position\n    for j in index...size\n      @arr[j] = @arr[j + 1]\n    end\n\n    # Update the number of elements\n    @size -= 1\n\n    # Return the removed element\n    num\n  end\n\n  ### Expand list capacity ###\n  def extend_capacity\n    # Create new array with length extend_ratio times original, copy original array to new array\n    arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n    # Add elements at the end\n    @capacity = arr.length\n  end\n\n  ### Convert list to array ###\n  def to_array\n    sz = size\n    # Elements enqueue\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":["Chapter 4. Arrays and Linked Lists","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   Random-Access Memory and Cache *","text":"

    In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent two physical layouts: \"contiguous storage\" and \"distributed storage\", respectively.

    In fact, physical structure largely determines the efficiency with which programs utilize memory and cache, which in turn affects the overall performance of algorithmic programs.

    ","path":["Chapter 4. Arrays and Linked Lists","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441-computer-storage-devices","level":2,"title":"4.4.1   Computer Storage Devices","text":"

    Computers include three types of storage devices: hard disk, random-access memory (RAM), and cache memory. The following table shows their different roles and performance characteristics in a computer system.

    Table 4-2   Computer Storage Devices

    Hard Disk RAM Cache Purpose Long-term storage of data, including operating systems, programs, and files Temporary storage of currently running programs and data being processed Storage of frequently accessed data and instructions to reduce CPU's accesses to memory Volatility Data is not lost after power-off Data is lost after power-off Data is lost after power-off Capacity Large, on the order of terabytes (TB) Small, on the order of gigabytes (GB) Very small, on the order of megabytes (MB) Speed Slow, hundreds to thousands of MB/s Fast, tens of GB/s Very fast, tens to hundreds of GB/s Cost (CNY/GB) Inexpensive, from a few tenths of a yuan to a few yuan per GB Expensive, from tens to hundreds of yuan per GB Very expensive, effectively bundled with the CPU package

    We can imagine the computer storage system as a pyramid, as shown in the diagram below. Storage devices closer to the top are faster, have smaller capacity, and are more expensive. This multi-layered design is deliberate, the result of careful consideration by computer scientists and engineers.

    • Hard disk cannot be easily replaced by RAM. First, data in memory is lost after power-off, making it unsuitable for long-term data storage. Second, memory is tens of times more expensive than hard disk, which makes it difficult to popularize in the consumer market.
    • Cache cannot simultaneously achieve large capacity and high speed. As the capacity of L1, L2, and L3 caches increases, their physical size becomes larger, and the physical distance between them and the CPU core increases, resulting in longer data transmission time and higher element access latency. With current technology, the multi-layered cache structure represents the best balance point between capacity, speed, and cost.

    Figure 4-9   Computer Storage System

    Tip

    The storage hierarchy of computers embodies a delicate balance among speed, capacity, and cost. In fact, such trade-offs are common across all industrial fields, requiring us to find the optimal balance point between different advantages and constraints.

    In summary, hard disks are used for long-term storage of large amounts of data, RAM is used to temporarily store the data being processed during program execution, and cache is used to store frequently accessed data and instructions, thereby improving program execution efficiency. The three work together to keep the computer system running efficiently.

    As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU. By intelligently loading data from RAM, it provides the CPU with high-speed access to data, significantly improving program execution efficiency and reducing reliance on slower RAM.

    Figure 4-10   Data Flow Among Hard Disk, RAM, and Cache

    ","path":["Chapter 4. Arrays and Linked Lists","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442-memory-efficiency-of-data-structures","level":2,"title":"4.4.2   Memory Efficiency of Data Structures","text":"

    In terms of memory space utilization, arrays and linked lists each have advantages and limitations.

    On one hand, memory is limited, and the same memory cannot be shared by multiple programs, so we hope data structures can utilize space as efficiently as possible. Array elements are tightly packed and do not require additional space to store references (pointers) between linked list nodes, thus having higher space efficiency. However, arrays need to allocate sufficient contiguous memory space at once, which may lead to memory waste, and array expansion requires additional time and space costs. In comparison, linked lists perform dynamic memory allocation and deallocation on a \"node\" basis, providing greater flexibility.

    On the other hand, during program execution, as memory is repeatedly allocated and freed, the degree of fragmentation of free memory becomes increasingly severe, leading to reduced memory utilization efficiency. Arrays, due to their contiguous storage approach, are relatively less prone to memory fragmentation. Conversely, linked list elements are distributed in storage, and frequent insertion and deletion operations are more likely to cause memory fragmentation.

    ","path":["Chapter 4. Arrays and Linked Lists","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443-cache-efficiency-of-data-structures","level":2,"title":"4.4.3   Cache Efficiency of Data Structures","text":"

    Although cache has much smaller space capacity than memory, it is much faster than memory and plays a crucial role in program execution speed. Since cache capacity is limited and can only store a small portion of frequently accessed data, when the CPU attempts to access data that is not in the cache, a cache miss occurs, and the CPU must load the required data from the slower memory.

    Clearly, the fewer \"cache misses,\" the higher the efficiency of CPU data reads and writes, and the better the program performance. We call the proportion of data that the CPU successfully obtains from the cache the cache hit rate, a metric typically used to measure cache efficiency.

    To achieve the highest efficiency possible, cache employs the following data loading mechanisms.

    • Cache lines: The cache does not store and load data on a byte-by-byte basis, but rather as cache lines. Compared to byte-by-byte transmission, cache line transmission is more efficient.
    • Prefetching mechanism: The processor attempts to predict data access patterns (e.g., sequential access, fixed-stride jumping access, etc.) and loads data into the cache according to specific patterns, thereby improving hit rate.
    • Spatial locality: If a piece of data is accessed, nearby data may also be accessed in the near future. Therefore, when the cache loads a particular piece of data, it also loads nearby data to improve hit rate.
    • Temporal locality: If a piece of data is accessed, it is likely to be accessed again in the near future. Cache leverages this principle by retaining recently accessed data to improve hit rate.

    In fact, arrays and linked lists differ in how efficiently they utilize cache, mainly in the following respects.

    • Space occupied: Linked-list elements occupy more space than array elements, so less useful data can fit in the cache.
    • Cache lines: Linked list data are scattered throughout memory, while cache loads \"by lines,\" so the proportion of invalid data loaded is higher.
    • Prefetching mechanism: Arrays have more \"predictable\" data access patterns than linked lists, making it easier for the system to guess which data will be loaded next.
    • Spatial locality: Arrays are stored in centralized memory space, so data near loaded data is more likely to be accessed soon.

    Overall, arrays have higher cache hit rates, thus they usually outperform linked lists in operation efficiency. This makes data structures implemented based on arrays more popular when solving algorithmic problems.

    It is important to note that high cache efficiency does not mean arrays are superior to linked lists in all cases. In practical applications, which data structure to choose should be determined based on specific requirements. For example, both arrays and linked lists can implement the \"stack\" data structure (which will be discussed in detail in the next chapter), but they are suitable for different scenarios.

    • When solving algorithm problems, we tend to prefer stack implementations based on arrays, because they provide higher operation efficiency and the ability of random access, at the cost of needing to pre-allocate a certain amount of memory space for the array.
    • If the data volume is very large, the dynamic nature is high, and the expected size of the stack is difficult to estimate, then a stack implementation based on linked lists is more suitable. Linked lists can distribute large amounts of data across different parts of memory and avoid the additional overhead produced by array expansion.
    ","path":["Chapter 4. Arrays and Linked Lists","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   Summary","text":"","path":["Chapter 4. Arrays and Linked Lists","4.5   Summary"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous storage and scattered storage. Their strengths and weaknesses complement each other.
    • Arrays support random access and use less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization.
    • Linked lists achieve efficient insertion and deletion of nodes by modifying references (pointers), and can flexibly adjust length; however, node access is inefficient and memory consumption is higher. Common linked list types include singly linked lists, circular linked lists, and doubly linked lists.
    • A list is an ordered collection of elements that supports insertion, deletion, search, and modification, typically implemented based on dynamic arrays. It retains the advantages of arrays while allowing flexible adjustment of length.
    • The emergence of lists has greatly improved the practicality of arrays, but it may also waste some memory space.
    • During program execution, data is primarily stored in memory. Arrays provide higher memory space efficiency, while linked lists offer greater flexibility in memory usage.
    • Caches provide fast data access to the CPU through mechanisms such as cache lines, prefetching, and spatial and temporal locality, significantly improving program execution efficiency.
    • Because arrays have higher cache hit rates, they are generally more efficient than linked lists. When choosing a data structure, appropriate selection should be made based on specific requirements and scenarios.
    ","path":["Chapter 4. Arrays and Linked Lists","4.5   Summary"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Does storing an array on the stack versus on the heap affect time efficiency and space efficiency?

    Arrays stored on the stack and on the heap are both stored in contiguous memory space, so data operation efficiency is basically the same. However, the stack and heap have their own characteristics, leading to the following differences.

    1. Allocation and deallocation efficiency: The stack is a relatively small piece of memory, with allocation automatically handled by the compiler; the heap is relatively larger and can be dynamically allocated in code, more prone to fragmentation. Therefore, allocation and deallocation operations on the heap are usually slower than on the stack.
    2. Size limitations: Stack memory is relatively small, and the heap size is generally limited by available memory. Therefore, the heap is more suitable for storing large arrays.
    3. Flexibility: The size of an array on the stack must be determined at compile time, while the size of an array on the heap can be determined dynamically at runtime.

    Q: Why do arrays require elements of the same type, while linked lists do not emphasize this requirement?

    Linked lists are composed of nodes, with nodes connected through references (pointers), and each node can store different types of data, such as int, double, string, object, etc.

    In contrast, array elements must be of the same type so that their positions can be determined by calculating offsets. For example, if an array contains both int and long types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different \"element sizes\".

    # element address = array base address (address of the first element) + element size * element index\n

    Q: After deleting node P, do we need to set P.next to None?

    It is not necessary to modify P.next. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter P. This means that node P has been removed from the linked list, and it doesn't matter where node P points to at this time—it won't affect the linked list.

    From an algorithms-and-problem-solving perspective, leaving the pointer connected is fine as long as the program logic is correct. From a standard-library implementation perspective, explicitly disconnecting it is safer and clearer. If it is not disconnected and the deleted node is not reclaimed properly, it may affect the reclamation of successor nodes.

    Q: In a linked list, the time complexity of insertion and deletion operations is \\(O(1)\\). However, both insertion and deletion require \\(O(n)\\) time to find the element; why isn't the time complexity \\(O(n)\\)?

    If the element is first found and then deleted, the time complexity is indeed \\(O(n)\\). However, the advantage of \\(O(1)\\) insertion and deletion in linked lists can be demonstrated in other applications. For example, a deque is well-suited for linked list implementation, where we maintain pointer variables always pointing to the head and tail nodes, with each insertion and deletion operation being \\(O(1)\\).

    Q: In the diagram \"Linked List Definition and Storage Methods\", does the light blue pointer node occupy a single memory address, or does it share equally with the node value?

    This diagram is a qualitative representation; a quantitative representation requires analysis based on the specific situation.

    • Different types of node values occupy different amounts of space, such as int, long, double, and instance objects, etc.
    • The amount of memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes.

    Q: Is appending an element at the end of a list always \\(O(1)\\)?

    If appending an element exceeds the list length, the list must first be expanded before adding. The system allocates a new block of memory and moves all elements from the original list to it, in which case the time complexity becomes \\(O(n)\\).

    Q: \"The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space\"—does this space waste refer to the memory occupied by additional variables such as capacity, length, and expansion factor?

    This space waste mainly has two aspects: on one hand, lists typically set an initial length, which we may not need to fully utilize; on the other hand, to prevent frequent expansion, expansion generally multiplies by a coefficient, such as \\(\\times 1.5\\). As a result, there will be many empty positions that we typically cannot completely fill.

    Q: In Python, after initializing n = [1, 2, 3], the addresses of these 3 elements are contiguous, but initializing m = [2, 1, 3] reveals that each element's id is not continuous; rather, they are the same as those in n. Since the addresses of these elements are not contiguous, is m still an array?

    If we replace list elements with linked list nodes n = [n1, n2, n3, n4, n5], usually these 5 node objects are also scattered throughout memory. However, given a list index, we can still obtain the node memory address in \\(O(1)\\) time, thereby accessing the corresponding node. This is because the array stores references to nodes, not the nodes themselves.

    Unlike many languages, numbers in Python are wrapped as objects, and lists store not the numbers themselves, but references to the numbers. Therefore, we find that the same numbers in two arrays have the same id, and the memory addresses of these numbers need not be contiguous.

    Q: C++ STL has std::list which has already implemented a doubly linked list, but it seems that some algorithm books don't use it directly. Is there a limitation?

    On one hand, we often prefer to use arrays for implementing algorithms and only use linked lists when necessary, mainly for two reasons.

    • Space overhead: Since each element requires two additional pointers (one for the previous element and one for the next element), std::list typically consumes more space than std::vector.
    • Cache unfriendliness: Since data is not stored contiguously, std::list has lower cache utilization. In general, std::vector has better performance.

    On the other hand, cases where linked lists are necessary mainly involve binary trees and graphs. Stacks and queues usually use the stack and queue provided by the programming language, rather than linked lists.

    Q: Does the operation res = [[0]] * n create a 2D list where each [0] is independent?

    No, they are not independent. In this 2D list, all the [0] are actually references to the same object. If we modify one element, we will find that all corresponding elements change accordingly.

    If we want each [0] in the 2D list to be independent, we can use res = [[0] for _ in range(n)] to achieve this. The principle of this approach is to initialize \\(n\\) independent [0] list objects.

    Q: Does the operation res = [0] * n create a list where each integer 0 is independent?

    In this list, all the integer zeros reference the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance.

    Although they all reference the same object, we can still modify each element in the list independently. This is because Python integers are \"immutable objects\". When we modify an element, we actually switch that element to reference a different object, rather than changing the original object itself.

    However, when list elements are \"mutable objects\" (such as lists, dictionaries, or class instances), modifying an element directly changes the object itself, and all elements referencing that object will have the same change.

    ","path":["Chapter 4. Arrays and Linked Lists","4.5   Summary"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"Chapter 13.   Backtracking","text":"

    Abstract

    We are like explorers in a maze, and may encounter difficulties on the path forward.

    The power of backtracking allows us to start over, keep trying, and eventually find the exit leading to light.

    ","path":["Chapter 13. Backtracking","Chapter 13.   Backtracking"],"tags":[]},{"location":"chapter_backtracking/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 13.1   Backtracking Algorithm
    • 13.2   Permutations Problem
    • 13.3   Subset-Sum Problem
    • 13.4   N-Queens Problem
    • 13.5   Summary
    ","path":["Chapter 13. Backtracking","Chapter 13.   Backtracking"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   Backtracking Algorithm","text":"

    The backtracking algorithm is a method for solving problems through exhaustive search. Its core idea is to start from an initial state and exhaustively search all possible solutions. When a correct solution is found, it is recorded. This process continues until a solution is found or all possible choices have been tried without finding a solution.

    The backtracking algorithm typically employs \"depth-first search\" to traverse the solution space. In the \"Binary Tree\" chapter, we mentioned that preorder, inorder, and postorder traversals all belong to depth-first search. Next, we will construct a backtracking problem using preorder traversal to progressively understand how the backtracking algorithm works.

    Example 1

    Given a binary tree, search and record all nodes with value \\(7\\), and return a list of these nodes.

    For this problem, we perform a preorder traversal of the tree and check whether the current node's value is \\(7\\). If it is, we add the node to the result list res. The relevant implementation is shown in the following figure and code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 1\"\"\"\n    if root is None:\n        return\n    if root.val == 7:\n        # Record solution\n        res.append(root)\n    pre_order(root.left)\n    pre_order(root.right)\n
    preorder_traversal_i_compact.cpp
    /* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(root);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
    preorder_traversal_i_compact.java
    /* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Record solution\n        res.add(root);\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n}\n
    preorder_traversal_i_compact.cs
    /* Preorder traversal: Example 1 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Record solution\n        res.Add(root);\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n
    preorder_traversal_i_compact.go
    /* Preorder traversal: Example 1 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    if (root.Val).(int) == 7 {\n        // Record solution\n        *res = append(*res, root)\n    }\n    preOrderI(root.Left, res)\n    preOrderI(root.Right, res)\n}\n
    preorder_traversal_i_compact.swift
    /* Preorder traversal: Example 1 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    if root.val == 7 {\n        // Record solution\n        res.append(root)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n
    preorder_traversal_i_compact.js
    /* Preorder traversal: Example 1 */\nfunction preOrder(root, res) {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Record solution\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
    preorder_traversal_i_compact.ts
    /* Preorder traversal: Example 1 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Record solution\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
    preorder_traversal_i_compact.dart
    /* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode? root, List<TreeNode> res) {\n  if (root == null) {\n    return;\n  }\n  if (root.val == 7) {\n    // Record solution\n    res.add(root);\n  }\n  preOrder(root.left, res);\n  preOrder(root.right, res);\n}\n
    preorder_traversal_i_compact.rs
    /* Preorder traversal: Example 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            // Record solution\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
    /* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    if (root->val == 7) {\n        // Record solution\n        res[resSize++] = root;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
    preorder_traversal_i_compact.kt
    /* Preorder traversal: Example 1 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(root)\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n}\n
    preorder_traversal_i_compact.rb
    ### Pre-order traversal: example 1 ###\ndef pre_order(root)\n  return unless root\n\n  # Record solution\n  $res << root if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\nend\n

    Figure 13-1   Search for nodes in preorder traversal

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311-attempt-and-backtrack","level":2,"title":"13.1.1   Attempt and Backtrack","text":"

    The reason it is called a backtracking algorithm is that it employs \"attempt\" and \"backtrack\" strategies when searching the solution space. When the algorithm encounters a state where it cannot continue forward or cannot find a solution that satisfies the constraints, it will undo the previous choice, return to a previous state, and try other possible choices.

    For Example 1, visiting each node represents an \"attempt\", while skipping over a leaf node or the return that brings the traversal back to the parent node represents a \"backtrack\".

    It is worth noting that backtracking is not limited to function returns alone. To illustrate this, let's extend Example 1 slightly.

    Example 2

    In a binary tree, search all nodes with value \\(7\\), and return the paths from the root node to these nodes.

    Based on the code from Example 1, we need to use a list path to record the path of visited nodes. When we reach a node with value \\(7\\), we copy path and add it to the result list res. After traversal is complete, res contains all the solutions. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 2\"\"\"\n    if root is None:\n        return\n    # Attempt\n    path.append(root)\n    if root.val == 7:\n        # Record solution\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Backtrack\n    path.pop()\n
    preorder_traversal_ii_compact.cpp
    /* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    // Attempt\n    path.push_back(root);\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    path.pop_back();\n}\n
    preorder_traversal_ii_compact.java
    /* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    // Attempt\n    path.add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Backtrack\n    path.remove(path.size() - 1);\n}\n
    preorder_traversal_ii_compact.cs
    /* Preorder traversal: Example 2 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    // Attempt\n    path.Add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Backtrack\n    path.RemoveAt(path.Count - 1);\n}\n
    preorder_traversal_ii_compact.go
    /* Preorder traversal: Example 2 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    // Attempt\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Record solution\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderII(root.Left, res, path)\n    preOrderII(root.Right, res, path)\n    // Backtrack\n    *path = (*path)[:len(*path)-1]\n}\n
    preorder_traversal_ii_compact.swift
    /* Preorder traversal: Example 2 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Attempt\n    path.append(root)\n    if root.val == 7 {\n        // Record solution\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Backtrack\n    path.removeLast()\n}\n
    preorder_traversal_ii_compact.js
    /* Preorder traversal: Example 2 */\nfunction preOrder(root, path, res) {\n    if (root === null) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
    preorder_traversal_ii_compact.ts
    /* Preorder traversal: Example 2 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    if (root === null) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
    preorder_traversal_ii_compact.dart
    /* Preorder traversal: Example 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  // Attempt\n  path.add(root);\n  if (root.val == 7) {\n    // Record solution\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Backtrack\n  path.removeLast();\n}\n
    preorder_traversal_ii_compact.rs
    /* Preorder traversal: Example 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        // Attempt\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Record solution\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        // Backtrack\n        path.pop();\n    }\n}\n
    preorder_traversal_ii_compact.c
    /* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    // Attempt\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Record solution\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    // Backtrack\n    pathSize--;\n}\n
    preorder_traversal_ii_compact.kt
    /* Preorder traversal: Example 2 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    // Attempt\n    path!!.add(root)\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Backtrack\n    path!!.removeAt(path!!.size - 1)\n}\n
    preorder_traversal_ii_compact.rb
    ### Pre-order traversal: example 2 ###\ndef pre_order(root)\n  return unless root\n\n  # Attempt\n  $path << root\n\n  # Record solution\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Backtrack\n  $path.pop\nend\n

    In each \"attempt\", we record the path by adding the current node to path; before \"backtracking\", we need to remove the node from path, to restore the state before this attempt.

    Observing the process shown in the following figure, we can understand attempt and backtrack as \"advance\" and \"undo\", two operations that are the reverse of each other.

    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 13-2   Attempt and backtrack

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312-pruning","level":2,"title":"13.1.2   Pruning","text":"

    Complex backtracking problems usually contain one or more constraints. Constraints can typically be used for \"pruning\".

    Example 3

    In a binary tree, search all nodes with value \\(7\\) and return the paths from the root node to these nodes, but require that the paths do not contain nodes with value \\(3\\).

    To satisfy the above constraints, we need to add pruning operations: during the search process, if we encounter a node with value \\(3\\), we return early and do not continue searching. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 3\"\"\"\n    # Pruning\n    if root is None or root.val == 3:\n        return\n    # Attempt\n    path.append(root)\n    if root.val == 7:\n        # Record solution\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Backtrack\n    path.pop()\n
    preorder_traversal_iii_compact.cpp
    /* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode *root) {\n    // Pruning\n    if (root == nullptr || root->val == 3) {\n        return;\n    }\n    // Attempt\n    path.push_back(root);\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    path.pop_back();\n}\n
    preorder_traversal_iii_compact.java
    /* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode root) {\n    // Pruning\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Attempt\n    path.add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Backtrack\n    path.remove(path.size() - 1);\n}\n
    preorder_traversal_iii_compact.cs
    /* Preorder traversal: Example 3 */\nvoid PreOrder(TreeNode? root) {\n    // Pruning\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Attempt\n    path.Add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Backtrack\n    path.RemoveAt(path.Count - 1);\n}\n
    preorder_traversal_iii_compact.go
    /* Preorder traversal: Example 3 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    // Pruning\n    if root == nil || root.Val == 3 {\n        return\n    }\n    // Attempt\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Record solution\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderIII(root.Left, res, path)\n    preOrderIII(root.Right, res, path)\n    // Backtrack\n    *path = (*path)[:len(*path)-1]\n}\n
    preorder_traversal_iii_compact.swift
    /* Preorder traversal: Example 3 */\nfunc preOrder(root: TreeNode?) {\n    // Pruning\n    guard let root = root, root.val != 3 else {\n        return\n    }\n    // Attempt\n    path.append(root)\n    if root.val == 7 {\n        // Record solution\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Backtrack\n    path.removeLast()\n}\n
    preorder_traversal_iii_compact.js
    /* Preorder traversal: Example 3 */\nfunction preOrder(root, path, res) {\n    // Pruning\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
    preorder_traversal_iii_compact.ts
    /* Preorder traversal: Example 3 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Pruning\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
    preorder_traversal_iii_compact.dart
    /* Preorder traversal: Example 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  // Attempt\n  path.add(root);\n  if (root.val == 7) {\n    // Record solution\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Backtrack\n  path.removeLast();\n}\n
    preorder_traversal_iii_compact.rs
    /* Preorder traversal: Example 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    // Pruning\n    if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n        return;\n    }\n    if let Some(node) = root {\n        // Attempt\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Record solution\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        // Backtrack\n        path.pop();\n    }\n}\n
    preorder_traversal_iii_compact.c
    /* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode *root) {\n    // Pruning\n    if (root == NULL || root->val == 3) {\n        return;\n    }\n    // Attempt\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Record solution\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    // Backtrack\n    pathSize--;\n}\n
    preorder_traversal_iii_compact.kt
    /* Preorder traversal: Example 3 */\nfun preOrder(root: TreeNode?) {\n    // Pruning\n    if (root == null || root._val == 3) {\n        return\n    }\n    // Attempt\n    path!!.add(root)\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Backtrack\n    path!!.removeAt(path!!.size - 1)\n}\n
    preorder_traversal_iii_compact.rb
    ### Pre-order traversal: example 3 ###\ndef pre_order(root)\n  # Pruning\n  return if !root || root.val == 3\n\n  # Attempt\n  $path.append(root)\n\n  # Record solution\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Backtrack\n  $path.pop\nend\n

    \"Pruning\" is a vivid term. As shown in the following figure, during the search process, we \"prune\" search branches that do not satisfy the constraints, avoiding many meaningless attempts and thus improving search efficiency.

    Figure 13-3   Pruning according to constraints

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313-framework-code","level":2,"title":"13.1.3   Framework Code","text":"

    Next, we attempt to extract a general framework centered on backtracking's \"attempt, backtrack, and pruning\" to improve code generality.

    In the following framework code, state represents the current state of the problem, and choices represents the choices available in the current state:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    def backtrack(state: State, choices: list[choice], res: list[state]):\n    \"\"\"Backtracking algorithm framework\"\"\"\n    # Check if it is a solution\n    if is_solution(state):\n        # Record the solution\n        record_solution(state, res)\n        # Stop searching\n        return\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: check if the choice is valid\n        if is_valid(state, choice):\n            # Attempt: make a choice and update the state\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice)\n
    /* Backtracking algorithm framework */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (Choice choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (Choice choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n    // Check if it is a solution\n    if (IsSolution(state)) {\n        // Record the solution\n        RecordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    foreach (Choice choice in choices) {\n        // Pruning: check if the choice is valid\n        if (IsValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            MakeChoice(state, choice);\n            Backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            UndoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n    // Check if it is a solution\n    if isSolution(state) {\n        // Record the solution\n        recordSolution(state, res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for _, choice := range choices {\n        // Pruning: check if the choice is valid\n        if isValid(state, choice) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n    // Check if it is a solution\n    if isSolution(state: state) {\n        // Record the solution\n        recordSolution(state: state, res: &res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if isValid(state: state, choice: choice) {\n            // Attempt: make a choice and update the state\n            makeChoice(state: &state, choice: choice)\n            backtrack(state: &state, choices: choices, res: &res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfunction backtrack(state, choices, res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (let choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (let choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n  // Check if it is a solution\n  if (isSolution(state)) {\n    // Record the solution\n    recordSolution(state, res);\n    // Stop searching\n    return;\n  }\n  // Traverse all choices\n  for (Choice choice in choices) {\n    // Pruning: check if the choice is valid\n    if (isValid(state, choice)) {\n      // Attempt: make a choice and update the state\n      makeChoice(state, choice);\n      backtrack(state, choices, res);\n      // Backtrack: undo the choice and restore to the previous state\n      undoChoice(state, choice);\n    }\n  }\n}\n
    /* Backtracking algorithm framework */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n    // Check if it is a solution\n    if is_solution(state) {\n        // Record the solution\n        record_solution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if is_valid(state, choice) {\n            // Attempt: make a choice and update the state\n            make_choice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res, numRes);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < numChoices; i++) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, &choices[i])) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, &choices[i]);\n            backtrack(state, choices, numChoices, res, numRes);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, &choices[i]);\n        }\n    }\n}\n
    /* Backtracking algorithm framework */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
    ### Backtracking algorithm framework ###\ndef backtrack(state, choices, res)\n    # Check if it is a solution\n    if is_solution?(state)\n        # Record the solution\n        record_solution(state, res)\n        return\n    end\n\n    # Traverse all choices\n    for choice in choices\n        # Pruning: check if the choice is valid\n        if is_valid?(state, choice)\n            # Attempt: make a choice and update the state\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice)\n        end\n    end\nend\n

    Next, we solve Example 3 based on the framework code. The state state is the node traversal path, the choices choices are the left and right child nodes of the current node, and the result res is a list of paths:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py
    def is_solution(state: list[TreeNode]) -> bool:\n    \"\"\"Check if the current state is a solution\"\"\"\n    return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n    \"\"\"Record solution\"\"\"\n    res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n    \"\"\"Check if the choice is valid under the current state\"\"\"\n    return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Update state\"\"\"\n    state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Restore state\"\"\"\n    state.pop()\n\ndef backtrack(\n    state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n    \"\"\"Backtracking algorithm: Example 3\"\"\"\n    # Check if it is a solution\n    if is_solution(state):\n        # Record solution\n        record_solution(state, res)\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: check if the choice is valid\n        if is_valid(state, choice):\n            # Attempt: make choice, update state\n            make_choice(state, choice)\n            # Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res)\n            # Backtrack: undo choice, restore to previous state\n            undo_choice(state, choice)\n
    preorder_traversal_iii_template.cpp
    /* Check if the current state is a solution */\nbool isSolution(vector<TreeNode *> &state) {\n    return !state.empty() && state.back()->val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n    res.push_back(state);\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n    return choice != nullptr && choice->val != 3;\n}\n\n/* Update state */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.push_back(choice);\n}\n\n/* Restore state */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.pop_back();\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (TreeNode *choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            vector<TreeNode *> nextChoices{choice->left, choice->right};\n            backtrack(state, nextChoices, res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    preorder_traversal_iii_template.java
    /* Check if the current state is a solution */\nboolean isSolution(List<TreeNode> state) {\n    return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.add(new ArrayList<>(state));\n}\n\n/* Check if the choice is valid under the current state */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n    state.add(choice);\n}\n\n/* Restore state */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n    state.remove(state.size() - 1);\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (TreeNode choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, Arrays.asList(choice.left, choice.right), res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
    preorder_traversal_iii_template.cs
    /* Check if the current state is a solution */\nbool IsSolution(List<TreeNode> state) {\n    return state.Count != 0 && state[^1].val == 7;\n}\n\n/* Record solution */\nvoid RecordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.Add(new List<TreeNode>(state));\n}\n\n/* Check if the choice is valid under the current state */\nbool IsValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid MakeChoice(List<TreeNode> state, TreeNode choice) {\n    state.Add(choice);\n}\n\n/* Restore state */\nvoid UndoChoice(List<TreeNode> state, TreeNode choice) {\n    state.RemoveAt(state.Count - 1);\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid Backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Check if it is a solution\n    if (IsSolution(state)) {\n        // Record solution\n        RecordSolution(state, res);\n    }\n    // Traverse all choices\n    foreach (TreeNode choice in choices) {\n        // Pruning: check if the choice is valid\n        if (IsValid(state, choice)) {\n            // Attempt: make choice, update state\n            MakeChoice(state, choice);\n            // Proceed to the next round of selection\n            Backtrack(state, [choice.left!, choice.right!], res);\n            // Backtrack: undo choice, restore to previous state\n            UndoChoice(state, choice);\n        }\n    }\n}\n
    preorder_traversal_iii_template.go
    /* Check if the current state is a solution */\nfunc isSolution(state *[]*TreeNode) bool {\n    return len(*state) != 0 && (*state)[len(*state)-1].Val == 7\n}\n\n/* Record solution */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n    *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* Check if the choice is valid under the current state */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n    return choice != nil && choice.Val != 3\n}\n\n/* Update state */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = append(*state, choice)\n}\n\n/* Restore state */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = (*state)[:len(*state)-1]\n}\n\n/* Backtracking algorithm: Example 3 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n    // Check if it is a solution\n    if isSolution(state) {\n        // Record solution\n        recordSolution(state, res)\n    }\n    // Traverse all choices\n    for _, choice := range *choices {\n        // Pruning: check if the choice is valid\n        if isValid(state, choice) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice)\n            // Proceed to the next round of selection\n            temp := make([]*TreeNode, 0)\n            temp = append(temp, choice.Left, choice.Right)\n            backtrackIII(state, &temp, res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
    preorder_traversal_iii_template.swift
    /* Check if the current state is a solution */\nfunc isSolution(state: [TreeNode]) -> Bool {\n    !state.isEmpty && state.last!.val == 7\n}\n\n/* Record solution */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n    res.append(state)\n}\n\n/* Check if the choice is valid under the current state */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -> Bool {\n    choice != nil && choice!.val != 3\n}\n\n/* Update state */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.append(choice)\n}\n\n/* Restore state */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.removeLast()\n}\n\n/* Backtracking algorithm: Example 3 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n    // Check if it is a solution\n    if isSolution(state: state) {\n        recordSolution(state: state, res: &res)\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if isValid(state: state, choice: choice) {\n            // Attempt: make choice, update state\n            makeChoice(state: &state, choice: choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
    preorder_traversal_iii_template.js
    /* Check if the current state is a solution */\nfunction isSolution(state) {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Record solution */\nfunction recordSolution(state, res) {\n    res.push([...state]);\n}\n\n/* Check if the choice is valid under the current state */\nfunction isValid(state, choice) {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Update state */\nfunction makeChoice(state, choice) {\n    state.push(choice);\n}\n\n/* Restore state */\nfunction undoChoice(state) {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfunction backtrack(state, choices, res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state);\n        }\n    }\n}\n
    preorder_traversal_iii_template.ts
    /* Check if the current state is a solution */\nfunction isSolution(state: TreeNode[]): boolean {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Record solution */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n    res.push([...state]);\n}\n\n/* Check if the choice is valid under the current state */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Update state */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n    state.push(choice);\n}\n\n/* Restore state */\nfunction undoChoice(state: TreeNode[]): void {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfunction backtrack(\n    state: TreeNode[],\n    choices: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state);\n        }\n    }\n}\n
    preorder_traversal_iii_template.dart
    /* Check if the current state is a solution */\nbool isSolution(List<TreeNode> state) {\n  return state.isNotEmpty && state.last.val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n  res.add(List.from(state));\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(List<TreeNode> state, TreeNode? choice) {\n  return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid makeChoice(List<TreeNode> state, TreeNode? choice) {\n  state.add(choice!);\n}\n\n/* Restore state */\nvoid undoChoice(List<TreeNode> state, TreeNode? choice) {\n  state.removeLast();\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(\n  List<TreeNode> state,\n  List<TreeNode?> choices,\n  List<List<TreeNode>> res,\n) {\n  // Check if it is a solution\n  if (isSolution(state)) {\n    // Record solution\n    recordSolution(state, res);\n  }\n  // Traverse all choices\n  for (TreeNode? choice in choices) {\n    // Pruning: check if the choice is valid\n    if (isValid(state, choice)) {\n      // Attempt: make choice, update state\n      makeChoice(state, choice);\n      // Proceed to the next round of selection\n      backtrack(state, [choice!.left, choice.right], res);\n      // Backtrack: undo choice, restore to previous state\n      undoChoice(state, choice);\n    }\n  }\n}\n
    preorder_traversal_iii_template.rs
    /* Check if the current state is a solution */\nfn is_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>) -> bool {\n    return !state.is_empty() && state.last().unwrap().borrow().val == 7;\n}\n\n/* Record solution */\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/* Check if the choice is valid under the current state */\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/* Update state */\nfn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {\n    state.push(choice);\n}\n\n/* Restore state */\nfn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 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    // Check if it is a solution\n    if is_solution(state) {\n        // Record solution\n        record_solution(state, res);\n    }\n    // Traverse all choices\n    for &choice in choices.iter() {\n        // Pruning: check if the choice is valid\n        if is_valid(state, choice) {\n            // Attempt: make choice, update state\n            make_choice(state, choice.unwrap().clone());\n            // Proceed to the next round of selection\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            // Backtrack: undo choice, restore to previous state\n            undo_choice(state, choice.unwrap().clone());\n        }\n    }\n}\n
    preorder_traversal_iii_template.c
    /* Check if the current state is a solution */\nbool isSolution(void) {\n    return pathSize > 0 && path[pathSize - 1]->val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(void) {\n    for (int i = 0; i < pathSize; i++) {\n        res[resSize][i] = path[i];\n    }\n    resSize++;\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(TreeNode *choice) {\n    return choice != NULL && choice->val != 3;\n}\n\n/* Update state */\nvoid makeChoice(TreeNode *choice) {\n    path[pathSize++] = choice;\n}\n\n/* Restore state */\nvoid undoChoice(void) {\n    pathSize--;\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(TreeNode *choices[2]) {\n    // Check if it is a solution\n    if (isSolution()) {\n        // Record solution\n        recordSolution();\n    }\n    // Traverse all choices\n    for (int i = 0; i < 2; i++) {\n        TreeNode *choice = choices[i];\n        // Pruning: check if the choice is valid\n        if (isValid(choice)) {\n            // Attempt: make choice, update state\n            makeChoice(choice);\n            // Proceed to the next round of selection\n            TreeNode *nextChoices[2] = {choice->left, choice->right};\n            backtrack(nextChoices);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice();\n        }\n    }\n}\n
    preorder_traversal_iii_template.kt
    /* Check if the current state is a solution */\nfun isSolution(state: MutableList<TreeNode?>): Boolean {\n    return state.isNotEmpty() && state[state.size - 1]?._val == 7\n}\n\n/* Record solution */\nfun recordSolution(state: MutableList<TreeNode?>?, res: MutableList<MutableList<TreeNode?>?>) {\n    res.add(state!!.toMutableList())\n}\n\n/* Check if the choice is valid under the current state */\nfun isValid(state: MutableList<TreeNode?>?, choice: TreeNode?): Boolean {\n    return choice != null && choice._val != 3\n}\n\n/* Update state */\nfun makeChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.add(choice)\n}\n\n/* Restore state */\nfun undoChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.removeLast()\n}\n\n/* Backtracking algorithm: Example 3 */\nfun backtrack(\n    state: MutableList<TreeNode?>,\n    choices: MutableList<TreeNode?>,\n    res: MutableList<MutableList<TreeNode?>?>\n) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res)\n    }\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice)\n            // Proceed to the next round of selection\n            backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
    preorder_traversal_iii_template.rb
    ### Check if current state is solution ###\ndef is_solution?(state)\n  !state.empty? && state.last.val == 7\nend\n\n### Record solution ###\ndef record_solution(state, res)\n  res << state.dup\nend\n\n### Check if choice is valid in current state ###\ndef is_valid?(state, choice)\n  choice && choice.val != 3\nend\n\n### Update state ###\ndef make_choice(state, choice)\n  state << choice\nend\n\n### Restore state ###\ndef undo_choice(state, choice)\n  state.pop\nend\n\n### Backtracking: example 3 ###\ndef backtrack(state, choices, res)\n  # Check if it is a solution\n  record_solution(state, res) if is_solution?(state)\n\n  # Traverse all choices\n  for choice in choices\n    # Pruning: check if the choice is valid\n    if is_valid?(state, choice)\n      # Attempt: make choice, update state\n      make_choice(state, choice)\n      # Proceed to the next round of selection\n      backtrack(state, [choice.left, choice.right], res)\n      # Backtrack: undo choice, restore to previous state\n      undo_choice(state, choice)\n    end\n  end\nend\n

    As per the problem statement, we should continue searching after finding a node with value \\(7\\). Therefore, we need to remove the return statement after recording the solution. The following figure compares the search process with and without the return statement.

    Figure 13-4   Comparison of search process with and without return statement

    Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but is more general. In fact, many backtracking problems can be solved within this framework. We only need to define state and choices for the specific problem and implement each method in the framework.

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314-common-terminology","level":2,"title":"13.1.4   Common Terminology","text":"

    To analyze algorithmic problems more clearly, we summarize the meanings of common terminology used in backtracking algorithms and provide corresponding examples from Example 3, as shown in the following table.

    Table 13-1   Common Backtracking Algorithm Terminology

    Term Definition Example 3 Solution (solution) A solution is an answer that satisfies the specific conditions of a problem; there may be one or more solutions All paths from root to nodes with value \\(7\\) that satisfy the constraint Constraint (constraint) A constraint is a condition in the problem that limits the feasibility of solutions, typically used for pruning Paths do not contain nodes with value \\(3\\) State (state) State represents the situation of a problem at a certain moment, including the choices already made The currently visited node path, i.e., the path list of nodes Attempt (attempt) An attempt is the process of exploring the solution space according to available choices, including making choices, updating state, and checking if it is a solution Recursively visit left (right) child nodes, add nodes to path, check if node value is \\(7\\) Backtrack (backtracking) Backtracking refers to undoing previous choices and returning to a previous state when encountering a state that does not satisfy constraints Stop searching when passing over leaf nodes, ending node visits, or encountering nodes with value \\(3\\); function returns Pruning (pruning) Pruning is a method of avoiding meaningless search paths according to problem characteristics and constraints, which can improve search efficiency When encountering a node with value \\(3\\), do not continue searching

    Tip

    The concepts of problem, solution, state, etc. are universal and appear in divide-and-conquer, backtracking, dynamic programming, greedy algorithms, and others.

    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315-advantages-and-limitations","level":2,"title":"13.1.5   Advantages and Limitations","text":"

    The backtracking algorithm is essentially a depth-first search algorithm that tries all possible solutions until it finds one that satisfies the conditions. The advantage of this approach is that it can find all possible solutions, and with reasonable pruning operations, it achieves high efficiency.

    However, when dealing with large-scale or complex problems, the running efficiency of the backtracking algorithm may be unacceptable.

    • Time: The backtracking algorithm usually needs to traverse all possibilities in the state space, and the time complexity can reach exponential or factorial order.
    • Space: During recursive calls, the current state needs to be saved (such as paths, auxiliary variables used for pruning, etc.), and when the depth is large, the space requirement can become very large.

    Nevertheless, the backtracking algorithm is still the best solution for certain search problems and constraint satisfaction problems. For these problems, since we cannot predict which choices will generate valid solutions, we must traverse all possible choices. In this case, the key is how to optimize efficiency. There are two common efficiency optimization methods.

    • Pruning: Avoid searching paths that are guaranteed not to produce solutions, thereby saving time and space.
    • Heuristic search: Introduce certain strategies or estimation values during the search process to prioritize searching paths that are most likely to produce valid solutions.
    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316-typical-backtracking-examples","level":2,"title":"13.1.6   Typical Backtracking Examples","text":"

    The backtracking algorithm can be used to solve many search problems, constraint satisfaction problems, and combinatorial optimization problems.

    Search problems: The goal of these problems is to find solutions that satisfy specific conditions.

    • Permutation problem: Given a set, find all possible permutations and combinations.
    • Subset sum problem: Given a set and a target sum, find all subsets in the set whose elements sum to the target.
    • Tower of Hanoi: Given three pegs and a series of disks of different sizes, move all disks from one peg to another, moving only one disk at a time, and never placing a larger disk on a smaller disk.

    Constraint satisfaction problems: The goal of these problems is to find solutions that satisfy all constraints.

    • N-Queens: Place \\(n\\) queens on an \\(n \\times n\\) chessboard such that they do not attack each other.
    • Sudoku: Fill numbers \\(1\\) to \\(9\\) in a \\(9 \\times 9\\) grid such that each row, column, and \\(3 \\times 3\\) subgrid contains no repeated digits.
    • Graph coloring: Given an undirected graph, color each vertex with the minimum number of colors such that adjacent vertices have different colors.

    Combinatorial optimization problems: The goal of these problems is to find an optimal solution that satisfies certain conditions in a combinatorial space.

    • 0-1 Knapsack: Given a set of items and a knapsack, each item has a value and weight. Under the knapsack capacity constraint, select items to maximize total value.
    • Traveling Salesman Problem: Starting from a point in a graph, visit all other points exactly once and return to the starting point, finding the shortest path.
    • Maximum Clique: Given an undirected graph, find the largest complete subgraph, i.e., a subgraph where any two vertices are connected by an edge.

    Note that for many combinatorial optimization problems, backtracking is not the optimal solution.

    • The 0-1 Knapsack problem is usually solved using dynamic programming to achieve higher time efficiency.
    • The Traveling Salesman Problem is a famous NP-Hard problem; common solutions include genetic algorithms and ant colony algorithms.
    • The Maximum Clique problem is a classical problem in graph theory and can be solved using heuristic algorithms such as greedy algorithms.
    ","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   N-Queens Problem","text":"

    Question

    According to the rules of chess, a queen can attack any piece in the same row, column, or diagonal. Given \\(n\\) queens and an \\(n \\times n\\) chessboard, find an arrangement such that no two queens can attack each other.

    As shown in Figure 13-15, when \\(n = 4\\), there are two solutions that can be found. From the perspective of the backtracking algorithm, an \\(n \\times n\\) chessboard has \\(n^2\\) squares, which provide all the choices choices. During the process of placing queens one by one, the chessboard state changes continuously, and the chessboard at each moment represents the state state.

    Figure 13-15   Solution to the 4-queens problem

    Figure 13-16 illustrates the three constraints of this problem: multiple queens cannot be in the same row, the same column, or on the same diagonal. It is worth noting that diagonals are divided into two types: the main diagonal \\ and the anti-diagonal /.

    Figure 13-16   Constraints of the n-queens problem

    ","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1-row-by-row-placement-strategy","level":3,"title":"1.   Row-By-Row Placement Strategy","text":"

    Since both the number of queens and the number of rows on the chessboard are \\(n\\), we can easily derive a conclusion: each row of the chessboard allows one and only one queen to be placed.

    This means we can adopt a row-by-row placement strategy: starting from the first row, place one queen in each row until the last row is completed.

    Figure 13-17 shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that violate the column or diagonal constraints are pruned.

    Figure 13-17   Row-by-row placement strategy

    Essentially, the row-by-row placement strategy serves a pruning function, as it avoids all search branches where multiple queens appear in the same row.

    ","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2-column-and-diagonal-pruning","level":3,"title":"2.   Column and Diagonal Pruning","text":"

    To satisfy the column constraint, we can use a boolean array cols of length \\(n\\) to record whether each column has a queen. Before each placement decision, we use cols to prune columns that already have queens, and dynamically update the state of cols during backtracking.

    Tip

    Please note that the origin of the matrix is located in the upper-left corner, where the row index increases from top to bottom, and the column index increases from left to right.

    So how do we handle diagonal constraints? Consider a square on the chessboard with row and column indices \\((row, col)\\). If we select a specific main diagonal in the matrix, we find that all squares on that diagonal have the same difference between their row and column indices, meaning that \\(row - col\\) is a constant value for all squares on the main diagonal.

    In other words, if two squares satisfy \\(row_1 - col_1 = row_2 - col_2\\), they must be on the same main diagonal. Using this pattern, we can use the array diags1 shown in Figure 13-18 to record whether there is a queen on each main diagonal.

    Similarly, for all squares on an anti-diagonal, the sum \\(row + col\\) is a constant value. We can likewise use the array diags2 to handle anti-diagonal constraints.

    Figure 13-18   Handling column and diagonal constraints

    ","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

    Please note that in an \\(n \\times n\\) square matrix, the range of \\(row - col\\) is \\([-n + 1, n - 1]\\), and the range of \\(row + col\\) is \\([0, 2n - 2]\\). Therefore, the number of both main diagonals and anti-diagonals is \\(2n - 1\\), meaning the length of both arrays diags1 and diags2 is \\(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    \"\"\"Backtracking algorithm: N queens\"\"\"\n    # When all rows are placed, record the solution\n    if row == n:\n        res.append([list(row) for row in state])\n        return\n    # Traverse all columns\n    for col in range(n):\n        # Calculate the main diagonal and anti-diagonal corresponding to this cell\n        diag1 = row - col + n - 1\n        diag2 = row + col\n        # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n            # Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            cols[col] = diags1[diag1] = diags2[diag2] = True\n            # Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            # Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n    \"\"\"Solve N queens\"\"\"\n    # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    state = [[\"#\" for _ in range(n)] for _ in range(n)]\n    cols = [False] * n  # Record whether there is a queen in the column\n    diags1 = [False] * (2 * n - 1)  # Record whether there is a queen on the main diagonal\n    diags2 = [False] * (2 * n - 1)  # Record whether there is a queen on the anti-diagonal\n    res = []\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n
    n_queens.cpp
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\n    if (row == n) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nvector<vector<vector<string>>> nQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    vector<vector<string>> state(n, vector<string>(n, \"#\"));\n    vector<bool> cols(n, false);           // Record whether there is a queen in the column\n    vector<bool> diags1(2 * n - 1, false); // Record whether there is a queen on the main diagonal\n    vector<bool> diags2(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n        boolean[] cols, boolean[] diags1, boolean[] diags2) {\n    // When all rows are placed, record the solution\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    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state.get(row).set(col, \"Q\");\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state.get(row).set(col, \"#\");\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nList<List<List<String>>> nQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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]; // Record whether there is a queen in the column\n    boolean[] diags1 = new boolean[2 * n - 1]; // Record whether there is a queen on the main diagonal\n    boolean[] diags2 = new boolean[2 * n - 1]; // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\nvoid Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,\n        bool[] cols, bool[] diags1, bool[] diags2) {\n    // When all rows are placed, record the solution\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    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nList<List<List<string>>> NQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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]; // Record whether there is a queen in the column\n    bool[] diags1 = new bool[2 * n - 1]; // Record whether there is a queen on the main diagonal\n    bool[] diags2 = new bool[2 * n - 1]; // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n    // When all rows are placed, record the solution\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    // Traverse all columns\n    for col := 0; col < n; col++ {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        diag1 := row - col + n - 1\n        diag2 := row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {\n            // Attempt: place the queen in this cell\n            (*state)[row][col] = \"Q\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n            // Place the next row\n            backtrack(row+1, n, state, res, cols, diags1, diags2)\n            // Backtrack: restore this cell to an empty cell\n            (*state)[row][col] = \"#\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n        }\n    }\n}\n\n/* Solve N queens */\nfunc nQueens(n int) [][][]string {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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    // Record whether there is a queen in the column\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
    /* Backtracking algorithm: N queens */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n    // When all rows are placed, record the solution\n    if row == n {\n        res.append(state)\n        return\n    }\n    // Traverse all columns\n    for col in 0 ..< n {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        let diag1 = row - col + n - 1\n        let diag2 = row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            cols[col] = true\n            diags1[diag1] = true\n            diags2[diag2] = true\n            // Place the next row\n            backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            cols[col] = false\n            diags1[diag1] = false\n            diags2[diag2] = false\n        }\n    }\n}\n\n/* Solve N queens */\nfunc nQueens(n: Int) -> [[[String]]] {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n    var cols = Array(repeating: false, count: n) // Record whether there is a queen in the column\n    var diags1 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the main diagonal\n    var diags2 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n    // When all rows are placed, record the solution\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Traverse all columns\n    for (let col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nfunction nQueens(n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Record whether there is a queen in the column\n    const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal\n    const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal\n    const res = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
    n_queens.ts
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Traverse all columns\n    for (let col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nfunction nQueens(n: number): string[][][] {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Record whether there is a queen in the column\n    const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal\n    const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal\n    const res: string[][][] = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
    n_queens.dart
    /* Backtracking algorithm: N queens */\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  // When all rows are placed, record the solution\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  // Traverse all columns\n  for (int col = 0; col < n; col++) {\n    // Calculate the main diagonal and anti-diagonal corresponding to this cell\n    int diag1 = row - col + n - 1;\n    int diag2 = row + col;\n    // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n    if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n      // Attempt: place the queen in this cell\n      state[row][col] = \"Q\";\n      cols[col] = true;\n      diags1[diag1] = true;\n      diags2[diag2] = true;\n      // Place the next row\n      backtrack(row + 1, n, state, res, cols, diags1, diags2);\n      // Backtrack: restore this cell to an empty cell\n      state[row][col] = \"#\";\n      cols[col] = false;\n      diags1[diag1] = false;\n      diags2[diag2] = false;\n    }\n  }\n}\n\n/* Solve N queens */\nList<List<List<String>>> nQueens(int n) {\n  // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n  List<List<String>> state = List.generate(n, (index) => List.filled(n, \"#\"));\n  List<bool> cols = List.filled(n, false); // Record whether there is a queen in the column\n  List<bool> diags1 = List.filled(2 * n - 1, false); // Record whether there is a queen on the main diagonal\n  List<bool> diags2 = List.filled(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\n    if row == n {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all columns\n    for col in 0..n {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        let diag1 = row + n - 1 - col;\n        let diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n        }\n    }\n}\n\n/* Solve N queens */\nfn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    let mut state: Vec<Vec<String>> = vec![vec![\"#\".to_string(); n]; n];\n    let mut cols = vec![false; n]; // Record whether there is a queen in the column\n    let mut diags1 = vec![false; 2 * n - 1]; // Record whether there is a queen on the main diagonal\n    let mut diags2 = vec![false; 2 * n - 1]; // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\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    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nchar ***nQueens(int n, int *returnSize) {\n    char state[MAX_SIZE][MAX_SIZE];\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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};           // Record whether there is a queen in the column\n    bool diags1[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the main diagonal\n    bool diags2[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the anti-diagonal\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
    /* Backtracking algorithm: N queens */\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    // When all rows are placed, record the solution\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    // Traverse all columns\n    for (col in 0..<n) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        val diag1 = row - col + n - 1\n        val diag2 = row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            diags2[diag2] = true\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            diags2[diag2] = false\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n        }\n    }\n}\n\n/* Solve N queens */\nfun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\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) // Record whether there is a queen in the column\n    val diags1 = BooleanArray(2 * n - 1) // Record whether there is a queen on the main diagonal\n    val diags2 = BooleanArray(2 * n - 1) // Record whether there is a queen on the anti-diagonal\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
    ### Backtracking: n queens ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n  # When all rows are placed, record the solution\n  if row == n\n    res << state.map { |row| row.dup }\n    return\n  end\n\n  # Traverse all columns\n  for col in 0...n\n    # Calculate the main diagonal and anti-diagonal corresponding to this cell\n    diag1 = row - col + n - 1\n    diag2 = row + col\n    # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n    if !cols[col] && !diags1[diag1] && !diags2[diag2]\n      # Attempt: place the queen in this cell\n      state[row][col] = \"Q\"\n      cols[col] = diags1[diag1] = diags2[diag2] = true\n      # Place the next row\n      backtrack(row + 1, n, state, res, cols, diags1, diags2)\n      # Backtrack: restore this cell to an empty cell\n      state[row][col] = \"#\"\n      cols[col] = diags1[diag1] = diags2[diag2] = false\n    end\n  end\nend\n\n### Solve n queens ###\ndef n_queens(n)\n  # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n  state = Array.new(n) { Array.new(n, \"#\") }\n  cols = Array.new(n, false) # Record whether there is a queen in the column\n  diags1 = Array.new(2 * n - 1, false) # Record whether there is a queen on the main diagonal\n  diags2 = Array.new(2 * n - 1, false) # Record whether there is a queen on the anti-diagonal\n  res = []\n  backtrack(0, n, state, res, cols, diags1, diags2)\n\n  res\nend\n

    Placing \\(n\\) queens row by row, considering the column constraint, from the first row to the last row there are \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\) choices, using \\(O(n!)\\) time. When recording a solution, it is necessary to copy the matrix state and add it to res, and the copy operation uses \\(O(n^2)\\) time. Therefore, the overall time complexity is \\(O(n! \\cdot n^2)\\). In practice, pruning based on diagonal constraints can also significantly reduce the search space, so the search efficiency is often better than the time complexity mentioned above.

    The array state uses \\(O(n^2)\\) space, and the arrays cols, diags1, and diags2 each use \\(O(n)\\) space. The maximum recursion depth is \\(n\\), using \\(O(n)\\) stack frame space. Therefore, the space complexity is \\(O(n^2)\\).

    ","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   Permutations Problem","text":"

    The permutations problem is a classic application of backtracking algorithms. It is defined as finding all possible arrangements of elements in a given collection (such as an array or string).

    Table 13-2 shows several example datasets, including input arrays and their corresponding permutations.

    Table 13-2   Permutations Examples

    Input Array All Permutations \\([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":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321-case-with-distinct-elements","level":2,"title":"13.2.1   Case with Distinct Elements","text":"

    Question

    Given an integer array with no duplicate elements, return all possible permutations.

    From the perspective of backtracking algorithms, we can imagine the process of generating permutations as the result of a series of choices. Suppose the input array is \\([1, 2, 3]\\). If we first choose \\(1\\), then choose \\(3\\), and finally choose \\(2\\), we obtain the permutation \\([1, 3, 2]\\). Backtracking means undoing a choice and then trying other choices.

    From the perspective of backtracking code, the candidate set choices consists of all elements in the input array, and the state state is the elements that have been chosen so far. Note that each element can only be chosen once, therefore all elements in state should be unique.

    As shown in Figure 13-5, we can unfold the search process into a recursion tree, where each node in the tree represents the current state state. Starting from the root node, after three rounds of choices, we reach a leaf node, and each leaf node corresponds to a permutation.

    Figure 13-5   Recursion tree of permutations

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1-pruning-duplicate-choices","level":3,"title":"1.   Pruning Duplicate Choices","text":"

    To ensure that each element is chosen only once, we consider introducing a boolean array selected, where selected[i] indicates whether choices[i] has been chosen. We implement the following pruning operation based on it.

    • After making a choice choices[i], we set selected[i] to \\(\\text{True}\\), indicating that it has been chosen.
    • When traversing the candidate list choices, we skip all nodes that have been chosen, which is pruning.

    As shown in Figure 13-6, suppose we choose \\(1\\) in the first round, \\(3\\) in the second round, and \\(2\\) in the third round. Then we need to prune the branch of element \\(1\\) in the second round and prune the branches of elements \\(1\\) and \\(3\\) in the third round.

    Figure 13-6   Pruning example of permutations

    Observing the above figure, we find that this pruning operation reduces the search space size from \\(O(n^n)\\) to \\(O(n!)\\).

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    After understanding the above information, we can fill in the blanks in the template code. To shorten the overall code, we do not implement each function in the template separately, but instead unfold them in the backtrack() function:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py
    def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Permutations I\"\"\"\n    # When the state length equals the number of elements, record the solution\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Traverse all choices\n    for i, choice in enumerate(choices):\n        # Pruning: do not allow repeated selection of elements\n        if not selected[i]:\n            # Attempt: make choice, update state\n            selected[i] = True\n            state.append(choice)\n            # Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            # Backtrack: undo choice, restore to previous state\n            selected[i] = False\n            state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n    \"\"\"Permutations I\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
    permutations_i.cpp
    /* Backtracking algorithm: Permutations I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push_back(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.add(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.Add(choice);\n            // Proceed to the next round of selection\n            Backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // When the state length equals the number of elements, record the solution\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Traverse all choices\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Pruning: do not allow repeated selection of elements\n        if !(*selected)[i] {\n            // Attempt: make choice, update state\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Proceed to the next round of selection\n            backtrackI(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // When the state length equals the number of elements, record the solution\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    for (i, choice) in choices.enumerated() {\n        // Pruning: do not allow repeated selection of elements\n        if !selected[i] {\n            // Attempt: make choice, update state\n            selected[i] = true\n            state.append(choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfunction backtrack(state, choices, selected, res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations I */\nfunction permutationsI(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
    permutations_i.ts
    /* Backtracking algorithm: Permutations I */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // When the state length equals the number of elements, record the solution\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Pruning: do not allow repeated selection of elements\n    if (!selected[i]) {\n      // Attempt: make choice, update state\n      selected[i] = true;\n      state.add(choice);\n      // Proceed to the next round of selection\n      backtrack(state, choices, selected, res);\n      // Backtrack: undo choice, restore to previous state\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // When the state length equals the number of elements, record the solution\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Traverse all choices\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if !selected[i] {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state.clone(), choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Permutations I */\nfn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new(); // State (subset)\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
    permutations_i.c
    /* Backtracking algorithm: Permutations I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // When the state length equals the number of elements, record the solution\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    // Traverse all choices\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Proceed to the next round of selection\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations I */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true\n            state.add(choice)\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Permutations 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
    ### Backtracking: permutations I ###\ndef backtrack(state, choices, selected, res)\n  # When the state length equals the number of elements, record the solution\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  choices.each_with_index do |choice, i|\n    # Pruning: do not allow repeated selection of elements\n    unless selected[i]\n      # Attempt: make choice, update state\n      selected[i] = true\n      state << choice\n      # Proceed to the next round of selection\n      backtrack(state, choices, selected, res)\n      # Backtrack: undo choice, restore to previous state\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Permutations I ###\ndef permutations_i(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322-case-with-duplicate-elements","level":2,"title":"13.2.2   Case with Duplicate Elements","text":"

    Question

    Given an integer array that may contain duplicate elements, return all unique permutations.

    Suppose the input array is \\([1, 1, 2]\\). To distinguish the two duplicate elements \\(1\\), we denote the second \\(1\\) as \\(\\hat{1}\\).

    As shown in Figure 13-7, half of the permutations generated by the above method are duplicates.

    Figure 13-7   Duplicate permutations

    So how do we remove duplicate permutations? The most direct approach is to use a hash set to directly deduplicate the permutation results. However, this is not elegant because the search branches that generate duplicate permutations are unnecessary and should be identified and pruned early, which can further improve algorithm efficiency.

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1-pruning-equal-elements","level":3,"title":"1.   Pruning Equal Elements","text":"

    Observe Figure 13-8. In the first round, choosing \\(1\\) or choosing \\(\\hat{1}\\) is equivalent. All permutations generated under these two choices are duplicates. Therefore, we should prune \\(\\hat{1}\\).

    Similarly, after choosing \\(2\\) in the first round, the \\(1\\) and \\(\\hat{1}\\) in the second round also produce duplicate branches, so the second round's \\(\\hat{1}\\) should also be pruned.

    Essentially, our goal is to ensure that multiple equal elements are chosen only once in a certain round of choices.

    Figure 13-8   Pruning duplicate permutations

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2-code-implementation_1","level":3,"title":"2.   Code Implementation","text":"

    Building on the code from the previous problem, we initialize a hash set duplicated in each round of choices to record which elements have already been tried in that round, and prune equal elements:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py
    def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Permutations II\"\"\"\n    # When the state length equals the number of elements, record the solution\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Traverse all choices\n    duplicated = set[int]()\n    for i, choice in enumerate(choices):\n        # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if not selected[i] and choice not in duplicated:\n            # Attempt: make choice, update state\n            duplicated.add(choice)  # Record the selected element value\n            selected[i] = True\n            state.append(choice)\n            # Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            # Backtrack: undo choice, restore to previous state\n            selected[i] = False\n            state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n    \"\"\"Permutations II\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
    permutations_ii.cpp
    /* Backtracking algorithm: Permutations II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    unordered_set<int> duplicated;\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n            // Attempt: make choice, update state\n            duplicated.emplace(choice); // Record the selected element value\n            selected[i] = true;\n            state.push_back(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Traverse all choices\n    Set<Integer> duplicated = new HashSet<Integer>();\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.add(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    HashSet<int> duplicated = [];\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.Contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.Add(choice); // Record the selected element value\n            selected[i] = true;\n            state.Add(choice);\n            // Proceed to the next round of selection\n            Backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // When the state length equals the number of elements, record the solution\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Traverse all choices\n    duplicated := make(map[int]struct{}, 0)\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if _, ok := duplicated[choice]; !ok && !(*selected)[i] {\n            // Attempt: make choice, update state\n            // Record the selected element value\n            duplicated[choice] = struct{}{}\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Proceed to the next round of selection\n            backtrackII(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // When the state length equals the number of elements, record the solution\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    var duplicated: Set<Int> = []\n    for (i, choice) in choices.enumerated() {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if !selected[i], !duplicated.contains(choice) {\n            // Attempt: make choice, update state\n            duplicated.insert(choice) // Record the selected element value\n            selected[i] = true\n            state.append(choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfunction backtrack(state, choices, selected, res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations II */\nfunction permutationsII(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
    permutations_ii.ts
    /* Backtracking algorithm: Permutations II */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // When the state length equals the number of elements, record the solution\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  Set<int> duplicated = {};\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n    if (!selected[i] && !duplicated.contains(choice)) {\n      // Attempt: make choice, update state\n      duplicated.add(choice); // Record the selected element value\n      selected[i] = true;\n      state.add(choice);\n      // Proceed to the next round of selection\n      backtrack(state, choices, selected, res);\n      // Backtrack: undo choice, restore to previous state\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // When the state length equals the number of elements, record the solution\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Traverse all choices\n    let mut duplicated = HashSet::<i32>::new();\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if !selected[i] && !duplicated.contains(&choice) {\n            // Attempt: make choice, update state\n            duplicated.insert(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state.clone(), choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // When the state length equals the number of elements, record the solution\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    // Traverse all choices\n    bool duplicated[MAX_SIZE] = {false};\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated[choice]) {\n            // Attempt: make choice, update state\n            duplicated[choice] = true; // Record the selected element value\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Proceed to the next round of selection\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n        }\n    }\n}\n\n/* Permutations 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
    /* Backtracking algorithm: Permutations II */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    val duplicated = HashSet<Int>()\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice) // Record the selected element value\n            selected[i] = true\n            state.add(choice)\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Permutations 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
    ### Backtracking: permutations II ###\ndef backtrack(state, choices, selected, res)\n  # When the state length equals the number of elements, record the solution\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  duplicated = Set.new\n  choices.each_with_index do |choice, i|\n    # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n    if !selected[i] && !duplicated.include?(choice)\n      # Attempt: make choice, update state\n      duplicated.add(choice)\n      selected[i] = true\n      state << choice\n      # Proceed to the next round of selection\n      backtrack(state, choices, selected, res)\n      # Backtrack: undo choice, restore to previous state\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Permutations II ###\ndef permutations_ii(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n

    Assuming elements are pairwise distinct, there are \\(n!\\) (factorial) permutations of \\(n\\) elements. When recording results, we need to copy a list of length \\(n\\), using \\(O(n)\\) time. Therefore, the time complexity is \\(O(n! \\cdot n)\\).

    The maximum recursion depth is \\(n\\), using \\(O(n)\\) stack frame space. selected uses \\(O(n)\\) space. At most \\(n\\) duplicated sets exist simultaneously, using \\(O(n^2)\\) space. Therefore, the space complexity is \\(O(n^2)\\).

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3-comparison-of-two-pruning-methods","level":3,"title":"3.   Comparison of Two Pruning Methods","text":"

    Note that although both selected and duplicated are used for pruning, they have different objectives.

    • Pruning duplicate choices: There is only one selected throughout the entire search process. It records which elements are included in the current state, and its purpose is to prevent an element from appearing repeatedly in state.
    • Pruning equal elements: Each round of choices (each backtrack function call) contains a duplicated set. It records which elements have been chosen in this round's iteration (the for loop), and its purpose is to ensure that equal elements are chosen only once.

    Figure 13-9 shows the effective scope of the two pruning conditions. Note that each node in the tree represents a choice, and the nodes on the path from the root to a leaf node form a permutation.

    Figure 13-9   Effective scope of two pruning conditions

    ","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   Subset-Sum Problem","text":"","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331-without-duplicate-elements","level":2,"title":"13.3.1   Without Duplicate Elements","text":"

    Question

    Given a positive integer array nums and a target positive integer target, find all possible combinations where the sum of elements in the combination equals target. The given array has no duplicate elements, and each element can be selected multiple times. Return these combinations in list form, where the list should not contain duplicate combinations.

    For example, given the set \\(\\{3, 4, 5\\}\\) and target integer \\(9\\), the solutions are \\(\\{3, 3, 3\\}, \\{4, 5\\}\\). Note the following two points:

    • Elements in the input set can be selected repeatedly without limit.
    • Subsets do not distinguish element order; for example, \\(\\{4, 5\\}\\) and \\(\\{5, 4\\}\\) are the same subset.
    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1-using-the-permutation-solution-as-a-reference","level":3,"title":"1.   Using the Permutation Solution as a Reference","text":"

    Similar to the permutation problem, we can view the process of generating subsets as the result of a series of choices and update the running sum during the selection process. When the sum equals target, we record the subset in the result list.

    Unlike the permutation problem, elements in this problem can be selected any number of times, so we do not need to use a selected boolean list to track whether an element has already been selected. With a few small changes to the permutation code, we obtain an initial solution:

    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    \"\"\"Backtracking algorithm: Subset sum I\"\"\"\n    # When the subset sum equals target, record the solution\n    if total == target:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    for i in range(len(choices)):\n        # Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target:\n            continue\n        # Attempt: make choice, update element sum total\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum I (including duplicate subsets)\"\"\"\n    state = []  # State (subset)\n    total = 0  # Subset sum\n    res = []  # Result list (subset list)\n    backtrack(state, target, total, nums, res)\n    return res\n
    subset_sum_i_naive.cpp
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    for (size_t i = 0; i < choices.size(); i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nvector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {\n    vector<int> state;       // State (subset)\n    int total = 0;           // Subset sum\n    vector<vector<int>> res; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.java
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    int total = 0; // Subset sum\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.cs
    /* Backtracking algorithm: Subset sum I */\nvoid Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.Length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<int>> SubsetSumINaive(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    int total = 0; // Subset sum\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.go
    /* Backtracking algorithm: Subset sum I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == total {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    for i := 0; i < len(*choices); i++ {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total+(*choices)[i] > target {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    total := 0              // Subset sum\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumINaive(total, target, &state, &nums, &res)\n    return res\n}\n
    subset_sum_i_naive.swift
    /* Backtracking algorithm: Subset sum I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if total == target {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    for i in choices.indices {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunc subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let total = 0 // Subset sum\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, total: total, choices: nums, res: &res)\n    return res\n}\n
    subset_sum_i_naive.js
    /* Backtracking algorithm: Subset sum I */\nfunction backtrack(state, target, total, choices, res) {\n    // When the subset sum equals target, record the solution\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    for (let i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunction subsetSumINaive(nums, target) {\n    const state = []; // State (subset)\n    const total = 0; // Subset sum\n    const res = []; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.ts
    /* Backtracking algorithm: Subset sum I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    total: number,\n    choices: number[],\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    for (let i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    const total = 0; // Subset sum\n    const res = []; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
    subset_sum_i_naive.dart
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  int total,\n  List<int> choices,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (total == target) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  for (int i = 0; i < choices.length; i++) {\n    // Pruning: if the subset sum exceeds target, skip this choice\n    if (total + choices[i] > target) {\n      continue;\n    }\n    // Attempt: make choice, update element sum total\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target, total + choices[i], choices, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<int>> subsetSumINaive(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  int total = 0; // Sum of elements\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, total, nums, res);\n  return res;\n}\n
    subset_sum_i_naive.rs
    /* Backtracking algorithm: Subset sum 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    // When the subset sum equals target, record the solution\n    if total == target {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    for i in 0..choices.len() {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    let total = 0; // Subset sum\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, total, nums, &mut res);\n    res\n}\n
    subset_sum_i_naive.c
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n    // When the subset sum equals target, record the solution\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    // Traverse all choices\n    for (int i = 0; i < choicesSize; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state[stateSize++] = choices[i];\n        // Proceed to the next round of selection\n        backtrack(target, total + choices[i], choices, choicesSize);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n    resSize = 0; // Initialize solution count to 0\n    backtrack(target, 0, nums, numsSize);\n}\n
    subset_sum_i_naive.kt
    /* Backtracking algorithm: Subset sum I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    total: Int,\n    choices: IntArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    for (i in choices.indices) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    val total = 0 // Subset sum\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, total, nums, res)\n    return res\n}\n
    subset_sum_i_naive.rb
    ### Backtracking: subset sum I ###\ndef backtrack(state, target, total, choices, res)\n  # When the subset sum equals target, record the solution\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  for i in 0...choices.length\n    # Pruning: if the subset sum exceeds target, skip this choice\n    next if total + choices[i] > target\n    # Attempt: make choice, update element sum total\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target, total + choices[i], choices, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum I (with duplicate subsets) ###\ndef subset_sum_i_naive(nums, target)\n  state = [] # State (subset)\n  total = 0 # Subset sum\n  res = [] # Result list (subset list)\n  backtrack(state, target, total, nums, res)\n  res\nend\n

    Running the above code on array \\([3, 4, 5]\\) with target value \\(9\\) produces \\([3, 3, 3], [4, 5], [5, 4]\\). Although we successfully found all subsets that sum to \\(9\\), there are duplicate subsets \\([4, 5]\\) and \\([5, 4]\\).

    This is because the search process distinguishes the order of selections, but subsets do not distinguish selection order. As shown in Figure 13-10, selecting 4 first and then 5 versus selecting 5 first and then 4 are different branches, but they correspond to the same subset.

    Figure 13-10   Subset search and boundary pruning

    To eliminate duplicate subsets, one straightforward idea is to deduplicate the result list. However, this approach is very inefficient for two reasons:

    • When there are many array elements, especially when target is large, the search process generates many duplicate subsets.
    • Comparing subsets (arrays) is very time-consuming, requiring sorting the arrays first, then comparing each element in them.
    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2-pruning-duplicate-subsets","level":3,"title":"2.   Pruning Duplicate Subsets","text":"

    We consider deduplication through pruning during the search process. Observing Figure 13-11, duplicate subsets occur when array elements are selected in different orders, as in the following cases:

    1. When the first and second rounds select \\(3\\) and \\(4\\) respectively, all subsets containing these two elements are generated, denoted as \\([3, 4, \\dots]\\).
    2. Afterward, when the first round selects \\(4\\), the second round should skip \\(3\\), because the subset \\([4, 3, \\dots]\\) generated by this choice is an exact duplicate of the subset generated in step 1.

    In the search process, each level's choices are tried from left to right, so the rightmost branches are pruned more.

    1. The first two rounds select \\(3\\) and \\(5\\), generating subset \\([3, 5, \\dots]\\).
    2. The first two rounds select \\(4\\) and \\(5\\), generating subset \\([4, 5, \\dots]\\).
    3. If the first round selects \\(5\\), the second round should skip \\(3\\) and \\(4\\), because subsets \\([5, 3, \\dots]\\) and \\([5, 4, \\dots]\\) are exact duplicates of the subsets described in steps 1. and 2.

    Figure 13-11   Different selection orders leading to duplicate subsets

    In summary, given an input array \\([x_1, x_2, \\dots, x_n]\\), let the selection sequence in the search process be \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\). This selection sequence must satisfy \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\); any selection sequence that does not satisfy this condition will cause duplicates and should be pruned.

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

    To implement this pruning, we initialize a variable start to indicate the starting point of traversal. After making choice \\(x_{i}\\), set the next round to start traversal from index \\(i\\). This ensures that the selection sequence satisfies \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\), guaranteeing subset uniqueness.

    In addition, we have made the following two optimizations to the code:

    • Before starting the search, first sort the array nums. When traversing all choices, end the loop immediately when the subset sum exceeds target, because subsequent elements are larger, and their subset sums must exceed target.
    • Omit the element sum variable total and use subtraction on target to track the sum of elements. Record the solution when target equals \\(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    \"\"\"Backtracking algorithm: Subset sum I\"\"\"\n    # When the subset sum equals target, record the solution\n    if target == 0:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    # Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in range(start, len(choices)):\n        # Pruning 1: if the subset sum exceeds target, end the loop directly\n        # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0:\n            break\n        # Attempt: make choice, update target, start\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum I\"\"\"\n    state = []  # State (subset)\n    nums.sort()  # Sort nums\n    start = 0  # Start point for traversal\n    res = []  # Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n
    subset_sum_i.cpp
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.size(); i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum I */\nvector<vector<int>> subsetSumI(vector<int> &nums, int target) {\n    vector<int> state;              // State (subset)\n    sort(nums.begin(), nums.end()); // Sort nums\n    int start = 0;                  // Start point for traversal\n    vector<vector<int>> res;        // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.java
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum I */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    Arrays.sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.cs
    /* Backtracking algorithm: Subset sum I */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.Length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum I */\nList<List<int>> SubsetSumI(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    Array.Sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.go
    /* Backtracking algorithm: Subset sum I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i := start; i < len(*choices); i++ {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Attempt: make choice, update target, start\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum I */\nfunc subsetSumI(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    sort.Ints(nums)         // Sort nums\n    start := 0              // Start point for traversal\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumI(start, target, &state, &nums, &res)\n    return res\n}\n
    subset_sum_i.swift
    /* Backtracking algorithm: Subset sum I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in choices.indices.dropFirst(start) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Attempt: make choice, update target, start\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum I */\nfunc subsetSumI(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let nums = nums.sorted() // Sort nums\n    let start = 0 // Start point for traversal\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
    subset_sum_i.js
    /* Backtracking algorithm: Subset sum I */\nfunction backtrack(state, target, choices, start, res) {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfunction subsetSumI(nums, target) {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.ts
    /* Backtracking algorithm: Subset sum I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_i.dart
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  // Pruning 2: start traversing from start to avoid generating duplicate subsets\n  for (int i = start; i < choices.length; i++) {\n    // Pruning 1: if the subset sum exceeds target, end the loop directly\n    // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Attempt: make choice, update target, start\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum I */\nList<List<int>> subsetSumI(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  nums.sort(); // Sort nums\n  int start = 0; // Start point for traversal\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
    subset_sum_i.rs
    /* Backtracking algorithm: Subset sum 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    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in start..choices.len() {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    nums.sort(); // Sort nums\n    let start = 0; // Start point for traversal\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
    subset_sum_i.c
    /* Backtracking algorithm: Subset sum I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // When the subset sum equals target, record the solution\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    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choicesSize; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Proceed to the next round of selection\n        backtrack(target - choices[i], choices, choicesSize, i);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum I */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n    qsort(nums, numsSize, sizeof(int), cmp); // Sort nums\n    int start = 0;                           // Start point for traversal\n    backtrack(target, nums, numsSize, start);\n}\n
    subset_sum_i.kt
    /* Backtracking algorithm: Subset sum I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (i in start..<choices.size) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum I */\nfun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    nums.sort() // Sort nums\n    val start = 0 // Start point for traversal\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
    subset_sum_i.rb
    ### Backtracking: subset sum I ###\ndef backtrack(state, target, choices, start, res)\n  # When the subset sum equals target, record the solution\n  if target.zero?\n    res << state.dup\n    return\n  end\n  # Traverse all choices\n  # Pruning 2: start traversing from start to avoid generating duplicate subsets\n  for i in start...choices.length\n    # Pruning 1: if the subset sum exceeds target, end the loop directly\n    # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    break if target - choices[i] < 0\n    # Attempt: make choice, update target, start\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum I ###\ndef subset_sum_i(nums, target)\n  state = [] # State (subset)\n  nums.sort! # Sort nums\n  start = 0 # Start point for traversal\n  res = [] # Result list (subset list)\n  backtrack(state, target, nums, start, res)\n  res\nend\n

    Figure 13-12 shows the complete backtracking process produced by running the above code on array \\([3, 4, 5]\\) with target value \\(9\\).

    Figure 13-12   Subset-sum I backtracking process

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332-with-duplicate-elements-in-array","level":2,"title":"13.3.2   With Duplicate Elements in Array","text":"

    Question

    Given a positive integer array nums and a target positive integer target, find all possible combinations where the sum of elements in the combination equals target. The given array may contain duplicate elements, and each element can be selected at most once. Return these combinations in list form, where the list should not contain duplicate combinations.

    Compared to the previous problem, the input array in this problem may contain duplicate elements, which introduces a new issue. For example, given array \\([4, \\hat{4}, 5]\\) and target value \\(9\\), the output of the existing code is \\([4, 5], [\\hat{4}, 5]\\), which contains duplicate subsets.

    The reason for this duplication is that equal elements are selected multiple times in a certain round. In Figure 13-13, the first round has three choices, two of which are \\(4\\), creating two duplicate search branches that output duplicate subsets. Similarly, the two \\(4\\)'s in the second round also produce duplicate subsets.

    Figure 13-13   Duplicate subsets caused by equal elements

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1-pruning-equal-elements","level":3,"title":"1.   Pruning Equal Elements","text":"

    To solve this problem, we need to limit equal elements to be selected only once in each round. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a given round of selection, if the current element equals the element to its left, then the same value has already been chosen in this round, so we skip the current element directly.

    At the same time, this problem specifies that each array element can only be selected once. Fortunately, we can also use the variable start to satisfy this constraint: after making choice \\(x_{i}\\), set the next round to start traversal from index \\(i + 1\\) onwards. This both eliminates duplicate subsets and avoids selecting elements multiple times.

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","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    \"\"\"Backtracking algorithm: Subset sum II\"\"\"\n    # When the subset sum equals target, record the solution\n    if target == 0:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    # Pruning 2: start traversing from start to avoid generating duplicate subsets\n    # Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in range(start, len(choices)):\n        # Pruning 1: if the subset sum exceeds target, end the loop directly\n        # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0:\n            break\n        # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start and choices[i] == choices[i - 1]:\n            continue\n        # Attempt: make choice, update target, start\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum II\"\"\"\n    state = []  # State (subset)\n    nums.sort()  # Sort nums\n    start = 0  # Start point for traversal\n    res = []  # Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n
    subset_sum_ii.cpp
    /* Backtracking algorithm: Subset sum II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.size(); i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum II */\nvector<vector<int>> subsetSumII(vector<int> &nums, int target) {\n    vector<int> state;              // State (subset)\n    sort(nums.begin(), nums.end()); // Sort nums\n    int start = 0;                  // Start point for traversal\n    vector<vector<int>> res;        // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.java
    /* Backtracking algorithm: Subset sum II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum II */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    Arrays.sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.cs
    /* Backtracking algorithm: Subset sum II */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.Length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum II */\nList<List<int>> SubsetSumII(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    Array.Sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.go
    /* Backtracking algorithm: Subset sum II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i := start; i < len(*choices); i++ {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start && (*choices)[i] == (*choices)[i-1] {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum II */\nfunc subsetSumII(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    sort.Ints(nums)         // Sort nums\n    start := 0              // Start point for traversal\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumII(start, target, &state, &nums, &res)\n    return res\n}\n
    subset_sum_ii.swift
    /* Backtracking algorithm: Subset sum II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in choices.indices.dropFirst(start) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start, choices[i] == choices[i - 1] {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum II */\nfunc subsetSumII(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let nums = nums.sorted() // Sort nums\n    let start = 0 // Start point for traversal\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
    subset_sum_ii.js
    /* Backtracking algorithm: Subset sum II */\nfunction backtrack(state, target, choices, start, res) {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfunction subsetSumII(nums, target) {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.ts
    /* Backtracking algorithm: Subset sum II */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
    subset_sum_ii.dart
    /* Backtracking algorithm: Subset sum II */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  // Pruning 2: start traversing from start to avoid generating duplicate subsets\n  // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n  for (int i = start; i < choices.length; i++) {\n    // Pruning 1: if the subset sum exceeds target, end the loop directly\n    // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n    if (i > start && choices[i] == choices[i - 1]) {\n      continue;\n    }\n    // Attempt: make choice, update target, start\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i + 1, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum II */\nList<List<int>> subsetSumII(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  nums.sort(); // Sort nums\n  int start = 0; // Start point for traversal\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
    subset_sum_ii.rs
    /* Backtracking algorithm: Subset sum 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    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in start..choices.len() {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start && choices[i] == choices[i - 1] {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    nums.sort(); // Sort nums\n    let start = 0; // Start point for traversal\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
    subset_sum_ii.c
    /* Backtracking algorithm: Subset sum II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // When the subset sum equals target, record the solution\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    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choicesSize; i++) {\n        // Pruning 1: Skip if subset sum exceeds target\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Proceed to the next round of selection\n        backtrack(target - choices[i], choices, choicesSize, i + 1);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum II */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n    // Sort nums\n    qsort(nums, numsSize, sizeof(int), cmp);\n    // Start backtracking\n    backtrack(target, nums, numsSize, 0);\n}\n
    subset_sum_ii.kt
    /* Backtracking algorithm: Subset sum II */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (i in start..<choices.size) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum II */\nfun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    nums.sort() // Sort nums\n    val start = 0 // Start point for traversal\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
    subset_sum_ii.rb
    ### Backtracking: subset sum II ###\ndef backtrack(state, target, choices, start, res)\n  # When the subset sum equals target, record the solution\n  if target.zero?\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  # Pruning 2: start traversing from start to avoid generating duplicate subsets\n  # Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n  for i in start...choices.length\n    # Pruning 1: if the subset sum exceeds target, end the loop directly\n    # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    break if target - choices[i] < 0\n    # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n    next if i > start && choices[i] == choices[i - 1]\n    # Attempt: make choice, update target, start\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i + 1, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum II ###\ndef subset_sum_ii(nums, target)\n  state = [] # State (subset)\n  nums.sort! # Sort nums\n  start = 0 # Start point for traversal\n  res = [] # Result list (subset list)\n  backtrack(state, target, nums, start, res)\n  res\nend\n

    Figure 13-14 shows the backtracking process for array \\([4, 4, 5]\\) with target value \\(9\\), which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works.

    Figure 13-14   Subset-sum II backtracking process

    ","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   Summary","text":"","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_backtracking/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • The backtracking algorithm is fundamentally an exhaustive search method. It finds solutions that meet specified conditions by performing a depth-first traversal of the solution space. During the search process, when a solution satisfying the conditions is found, it is recorded. The search ends either after finding all solutions or when the traversal is complete.
    • The backtracking algorithm search process consists of two parts: attempting and backtracking. It tries various choices through depth-first search. When encountering situations that violate constraints, it reverts the previous choice, returns to the previous state, and continues exploring other options. Attempting and backtracking are operations in opposite directions.
    • Backtracking problems typically contain multiple constraints, which can be utilized to implement pruning operations. Pruning can terminate unnecessary search branches early, significantly improving search efficiency.
    • The backtracking algorithm is primarily used to solve search problems and constraint satisfaction problems. While combinatorial optimization problems can be solved with backtracking, there are often more efficient or better-performing solutions available.
    • The permutation problem aims to find all possible permutations of elements in a given set. We use an array to record whether each element has been selected, thereby pruning search branches that attempt to select the same element repeatedly, ensuring each element is selected exactly once.
    • In the permutation problem, if the set contains duplicate elements, the final result will contain duplicate permutations. We need to impose a constraint so that equal elements can only be selected once per round, which is typically achieved using a hash set.
    • The subset-sum problem aims to find all subsets of a given set that sum to a target value. Since the set is unordered but the search process outputs results in all orders, duplicate subsets are generated. We sort the data before backtracking and use a variable to indicate the starting point of each round's traversal, thereby pruning search branches that generate duplicate subsets.
    • For the subset-sum problem, equal elements in the array produce duplicate subsets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round.
    • The \\(n\\) queens problem aims to find placements of \\(n\\) queens on an \\(n \\times n\\) chessboard such that no two queens can attack each other. The constraints of this problem include row constraints, column constraints, and main and anti-diagonal constraints. To satisfy row constraints, we adopt a row-by-row placement strategy, ensuring exactly one queen is placed in each row.
    • The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether each column has a queen, thereby indicating whether a selected cell is valid. For diagonal constraints, we use two arrays to separately record whether queens exist on each main or anti-diagonal. The challenge lies in finding the row-column index pattern that characterizes cells on the same main (anti-)diagonal.
    ","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: How can we understand the relationship between backtracking and recursion?

    Overall, backtracking is an algorithmic strategy, while recursion is better viewed as a tool.

    • Backtracking is typically implemented with recursion. However, backtracking is only one application of recursion, specifically its use in search problems.
    • The structure of recursion reflects a problem-solving paradigm based on decomposing a problem into subproblems, and it is commonly used in divide-and-conquer, backtracking, and dynamic programming (memoized recursion).
    ","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"Chapter 2.   Complexity Analysis","text":"

    Abstract

    Complexity analysis is like a space-time guide in the vast universe of algorithms.

    It leads us to explore deeply within the two dimensions of time and space, seeking more elegant solutions.

    ","path":["Chapter 2. Complexity Analysis","Chapter 2.   Complexity Analysis"],"tags":[]},{"location":"chapter_computational_complexity/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 2.1   Algorithm Efficiency Evaluation
    • 2.2   Iteration and Recursion
    • 2.3   Time Complexity
    • 2.4   Space Complexity
    • 2.5   Summary
    ","path":["Chapter 2. Complexity Analysis","Chapter 2.   Complexity Analysis"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   Iteration and Recursion","text":"

    In algorithms, repeatedly executing a task is very common and closely related to complexity analysis. Therefore, before introducing time complexity and space complexity, let's first understand how to implement repeated task execution in programs, namely the two basic program control structures: iteration and recursion.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221-iteration","level":2,"title":"2.2.1   Iteration","text":"

    Iteration is a control structure for repeatedly executing a task. In iteration, a program repeatedly executes a segment of code under certain conditions until those conditions are no longer satisfied.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for-loop","level":3,"title":"1.   For Loop","text":"

    The for loop is one of the most common forms of iteration, suitable for use when the number of iterations is known in advance.

    The following function implements the summation \\(1 + 2 + \\dots + n\\) using a for loop, with the result stored in the variable res. Note that in Python, range(a, b) corresponds to a \"left-closed, right-open\" interval, with the traversal range being \\(a, a + 1, \\dots, b-1\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def for_loop(n: int) -> int:\n    \"\"\"for loop\"\"\"\n    res = 0\n    # Sum 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        res += i\n    return res\n
    iteration.cpp
    /* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 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 loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 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 loop */\nint ForLoop(int n) {\n    int res = 0;\n    // Sum 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 loop */\nfunc forLoop(n int) int {\n    res := 0\n    // Sum 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        res += i\n    }\n    return res\n}\n
    iteration.swift
    /* for loop */\nfunc forLoop(n: Int) -> Int {\n    var res = 0\n    // Sum 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        res += i\n    }\n    return res\n}\n
    iteration.js
    /* for loop */\nfunction forLoop(n) {\n    let res = 0;\n    // Sum 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 loop */\nfunction forLoop(n: number): number {\n    let res = 0;\n    // Sum 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 loop */\nint forLoop(int n) {\n  int res = 0;\n  // Sum 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 loop */\nfn for_loop(n: i32) -> i32 {\n    let mut res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for i in 1..=n {\n        res += i;\n    }\n    res\n}\n
    iteration.c
    /* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 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 loop */\nfun forLoop(n: Int): Int {\n    var res = 0\n    // Sum 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        res += i\n    }\n    return res\n}\n
    iteration.rb
    ### for loop ###\ndef for_loop(n)\n  res = 0\n\n  # Sum 1, 2, ..., n-1, n\n  for i in 1..n\n    res += i\n  end\n\n  res\nend\n

    Figure 2-1 shows the flowchart of this summation function.

    Figure 2-1   Flowchart of the summation function

    The number of operations in this summation function is proportional to the input data size \\(n\\), or has a \"linear relationship\". In fact, time complexity describes precisely this \"linear relationship\". Related content will be introduced in detail in the next section.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while-loop","level":3,"title":"2.   While Loop","text":"

    Similar to the for loop, the while loop is also a method for implementing iteration. In a while loop, the program first checks the condition in each round; if the condition is true, it continues execution, otherwise it ends the loop.

    Below we use a while loop to implement the summation \\(1 + 2 + \\dots + n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def while_loop(n: int) -> int:\n    \"\"\"while loop\"\"\"\n    res = 0\n    i = 1  # Initialize condition variable\n    # Sum 1, 2, ..., n-1, n\n    while i <= n:\n        res += i\n        i += 1  # Update condition variable\n    return res\n
    iteration.cpp
    /* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.java
    /* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.cs
    /* while loop */\nint WhileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i += 1; // Update condition variable\n    }\n    return res;\n}\n
    iteration.go
    /* while loop */\nfunc whileLoop(n int) int {\n    res := 0\n    // Initialize condition variable\n    i := 1\n    // Sum 1, 2, ..., n-1, n\n    for i <= n {\n        res += i\n        // Update condition variable\n        i++\n    }\n    return res\n}\n
    iteration.swift
    /* while loop */\nfunc whileLoop(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while i <= n {\n        res += i\n        i += 1 // Update condition variable\n    }\n    return res\n}\n
    iteration.js
    /* while loop */\nfunction whileLoop(n) {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.ts
    /* while loop */\nfunction whileLoop(n: number): number {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.dart
    /* while loop */\nint whileLoop(int n) {\n  int res = 0;\n  int i = 1; // Initialize condition variable\n  // Sum 1, 2, ..., n-1, n\n  while (i <= n) {\n    res += i;\n    i++; // Update condition variable\n  }\n  return res;\n}\n
    iteration.rs
    /* while loop */\nfn while_loop(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Initialize condition variable\n\n    // Sum 1, 2, ..., n-1, n\n    while i <= n {\n        res += i;\n        i += 1; // Update condition variable\n    }\n    res\n}\n
    iteration.c
    /* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
    iteration.kt
    /* while loop */\nfun whileLoop(n: Int): Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i\n        i++ // Update condition variable\n    }\n    return res\n}\n
    iteration.rb
    ### while loop ###\ndef while_loop(n)\n  res = 0\n  i = 1 # Initialize condition variable\n\n  # Sum 1, 2, ..., n-1, n\n  while i <= n\n    res += i\n    i += 1 # Update condition variable\n  end\n\n  res\nend\n

    The while loop has greater flexibility than the for loop. In a while loop, we can freely design the initialization and update steps of the condition variable.

    For example, in the following code, the condition variable \\(i\\) is updated twice per round, which is not convenient to implement using a for loop:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def while_loop_ii(n: int) -> int:\n    \"\"\"while loop (two updates)\"\"\"\n    res = 0\n    i = 1  # Initialize condition variable\n    # Sum 1, 4, 10, ...\n    while i <= n:\n        res += i\n        # Update condition variable\n        i += 1\n        i *= 2\n    return res\n
    iteration.cpp
    /* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.java
    /* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.cs
    /* while loop (two updates) */\nint WhileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i += 1; \n        i *= 2;\n    }\n    return res;\n}\n
    iteration.go
    /* while loop (two updates) */\nfunc whileLoopII(n int) int {\n    res := 0\n    // Initialize condition variable\n    i := 1\n    // Sum 1, 4, 10, ...\n    for i <= n {\n        res += i\n        // Update condition variable\n        i++\n        i *= 2\n    }\n    return res\n}\n
    iteration.swift
    /* while loop (two updates) */\nfunc whileLoopII(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while i <= n {\n        res += i\n        // Update condition variable\n        i += 1\n        i *= 2\n    }\n    return res\n}\n
    iteration.js
    /* while loop (two updates) */\nfunction whileLoopII(n) {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.ts
    /* while loop (two updates) */\nfunction whileLoopII(n: number): number {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.dart
    /* while loop (two updates) */\nint whileLoopII(int n) {\n  int res = 0;\n  int i = 1; // Initialize condition variable\n  // Sum 1, 4, 10, ...\n  while (i <= n) {\n    res += i;\n    // Update condition variable\n    i++;\n    i *= 2;\n  }\n  return res;\n}\n
    iteration.rs
    /* while loop (two updates) */\nfn while_loop_ii(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Initialize condition variable\n\n    // Sum 1, 4, 10, ...\n    while i <= n {\n        res += i;\n        // Update condition variable\n        i += 1;\n        i *= 2;\n    }\n    res\n}\n
    iteration.c
    /* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
    iteration.kt
    /* while loop (two updates) */\nfun whileLoopII(n: Int): Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i\n        // Update condition variable\n        i++\n        i *= 2\n    }\n    return res\n}\n
    iteration.rb
    ### while loop (two updates) ###\ndef while_loop_ii(n)\n  res = 0\n  i = 1 # Initialize condition variable\n\n  # Sum 1, 4, 10, ...\n  while i <= n\n    res += i\n    # Update condition variable\n    i += 1\n    i *= 2\n  end\n\n  res\nend\n

    Overall, for loops have more compact code, while while loops are more flexible; both can implement iterative structures. The choice of which to use should be determined based on the requirements of the specific problem.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3-nested-loops","level":3,"title":"3.   Nested Loops","text":"

    We can nest one loop structure inside another. Below is an example using for loops:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def nested_for_loop(n: int) -> str:\n    \"\"\"Nested for loop\"\"\"\n    res = \"\"\n    # Loop i = 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        # Loop 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
    /* Nested for loop */\nstring nestedForLoop(int n) {\n    ostringstream res;\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        // Loop 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
    /* Nested for loop */\nString nestedForLoop(int n) {\n    StringBuilder res = new StringBuilder();\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nstring NestedForLoop(int n) {\n    StringBuilder res = new();\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nfunc nestedForLoop(n int) string {\n    res := \"\"\n    // Loop i = 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= n; j++ {\n            // Loop j = 1, 2, ..., n-1, n\n            res += fmt.Sprintf(\"(%d, %d), \", i, j)\n        }\n    }\n    return res\n}\n
    iteration.swift
    /* Nested for loop */\nfunc nestedForLoop(n: Int) -> String {\n    var res = \"\"\n    // Loop i = 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        // Loop 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
    /* Nested for loop */\nfunction nestedForLoop(n) {\n    let res = '';\n    // Loop i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nfunction nestedForLoop(n: number): string {\n    let res = '';\n    // Loop i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nString nestedForLoop(int n) {\n  String res = \"\";\n  // Loop i = 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    // Loop 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
    /* Nested for loop */\nfn nested_for_loop(n: i32) -> String {\n    let mut res = vec![];\n    // Loop i = 1, 2, ..., n-1, n\n    for i in 1..=n {\n        // Loop 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
    /* Nested for loop */\nchar *nestedForLoop(int n) {\n    // n * n is the number of points, \"(i, j), \" string max length is 6+10*2, plus extra space for null character \\0\n    int size = n * n * 26 + 1;\n    char *res = malloc(size * sizeof(char));\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop 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
    /* Nested for loop */\nfun nestedForLoop(n: Int): String {\n    val res = StringBuilder()\n    // Loop i = 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        // Loop 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
    ### Nested for loop ###\ndef nested_for_loop(n)\n  res = \"\"\n\n  # Loop i = 1, 2, ..., n-1, n\n  for i in 1..n\n    # Loop 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

    Figure 2-2 shows the flowchart of this nested loop.

    Figure 2-2   Flowchart of nested loops

    In this case, the number of operations of the function is proportional to \\(n^2\\), or the algorithm's running time has a \"quadratic relationship\" with the input data size \\(n\\).

    We can continue adding nested loops, where each additional level of nesting can be viewed as an increase in dimensionality, raising the time complexity to a \"cubic relationship\", a \"quartic relationship\", and so on.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222-recursion","level":2,"title":"2.2.2   Recursion","text":"

    Recursion is an algorithmic strategy that solves problems by having a function call itself. It mainly consists of two phases.

    1. Descend: The program continuously calls itself deeper, usually passing in smaller or more simplified parameters, until reaching a \"termination condition\".
    2. Ascend: After triggering the \"termination condition\", the program returns layer by layer from the deepest recursive function, aggregating the result of each layer.

    From an implementation perspective, recursive code mainly consists of three elements.

    1. Termination condition: Used to determine when to switch from \"descending\" to \"ascending\".
    2. Recursive call: Corresponds to \"descending\", where the function calls itself, usually with smaller or more simplified parameters.
    3. Return result: Corresponds to \"ascending\", returning the result of the current recursion level to the previous layer.

    Observe the following code. We only need to call the function recur(n) to complete the calculation of \\(1 + 2 + \\dots + n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
    def recur(n: int) -> int:\n    \"\"\"Recursion\"\"\"\n    # Termination condition\n    if n == 1:\n        return 1\n    # Recurse: recursive call\n    res = recur(n - 1)\n    # Return: return result\n    return n + res\n
    recursion.cpp
    /* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.java
    /* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.cs
    /* Recursion */\nint Recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = Recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.go
    /* Recursion */\nfunc recur(n int) int {\n    // Termination condition\n    if n == 1 {\n        return 1\n    }\n    // Recurse: recursive call\n    res := recur(n - 1)\n    // Return: return result\n    return n + res\n}\n
    recursion.swift
    /* Recursion */\nfunc recur(n: Int) -> Int {\n    // Termination condition\n    if n == 1 {\n        return 1\n    }\n    // Recurse: recursive call\n    let res = recur(n: n - 1)\n    // Return: return result\n    return n + res\n}\n
    recursion.js
    /* Recursion */\nfunction recur(n) {\n    // Termination condition\n    if (n === 1) return 1;\n    // Recurse: recursive call\n    const res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.ts
    /* Recursion */\nfunction recur(n: number): number {\n    // Termination condition\n    if (n === 1) return 1;\n    // Recurse: recursive call\n    const res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.dart
    /* Recursion */\nint recur(int n) {\n  // Termination condition\n  if (n == 1) return 1;\n  // Recurse: recursive call\n  int res = recur(n - 1);\n  // Return: return result\n  return n + res;\n}\n
    recursion.rs
    /* Recursion */\nfn recur(n: i32) -> i32 {\n    // Termination condition\n    if n == 1 {\n        return 1;\n    }\n    // Recurse: recursive call\n    let res = recur(n - 1);\n    // Return: return result\n    n + res\n}\n
    recursion.c
    /* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
    recursion.kt
    /* Recursion */\nfun recur(n: Int): Int {\n    // Termination condition\n    if (n == 1)\n        return 1\n    // Descend: recursive call\n    val res = recur(n - 1)\n    // Return: return result\n    return n + res\n}\n
    recursion.rb
    ### Recursion ###\ndef recur(n)\n  # Termination condition\n  return 1 if n == 1\n  # Recurse: recursive call\n  res = recur(n - 1)\n  # Return: return result\n  n + res\nend\n

    Figure 2-3 shows the recursive process of this function.

    Figure 2-3   Recursive process of the summation function

    Although from a computational perspective, iteration and recursion can achieve the same results, they represent two completely different paradigms for thinking about and solving problems.

    • Iteration: Solves problems \"bottom-up\". Starting from the most basic steps, these steps are then repeatedly executed or accumulated until the task is complete.
    • Recursion: Solves problems \"top-down\". The original problem is decomposed into smaller subproblems that have the same form as the original problem. These subproblems continue to be decomposed into even smaller subproblems until reaching the base case (where the solution is known).

    Taking the above summation function as an example, let the problem be \\(f(n) = 1 + 2 + \\dots + n\\).

    • Iteration: Simulates the summation process in a loop, traversing from \\(1\\) to \\(n\\), performing the summation operation in each round to obtain \\(f(n)\\).
    • Recursion: Decomposes the problem into the subproblem \\(f(n) = n + f(n-1)\\), continuously decomposing (recursively) until terminating at the base case \\(f(1) = 1\\).
    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-call-stack","level":3,"title":"1.   Call Stack","text":"

    Each time a recursive function calls itself, the system allocates memory for the newly invoked function to store local variables, call addresses, and other information. This leads to two consequences.

    • The function's context data is stored in a memory area called \"stack frame space\", which is not released until the function returns. Therefore, recursion usually consumes more memory space than iteration.
    • Recursive function calls incur additional overhead. Therefore, recursion is usually less time-efficient than loops.

    As shown in Figure 2-4, before the termination condition is triggered, there are \\(n\\) unreturned recursive functions existing simultaneously, with a recursion depth of \\(n\\).

    Figure 2-4   Recursion call depth

    In practice, the recursion depth allowed by programming languages is usually limited, and excessively deep recursion may lead to stack overflow errors.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-tail-recursion","level":3,"title":"2.   Tail Recursion","text":"

    Interestingly, if a function makes the recursive call as the very last step before returning, the compiler or interpreter may optimize it so that its space efficiency is comparable to iteration. This case is called tail recursion.

    • Regular recursion: When a function returns to the previous level, it needs to continue executing code, so the system needs to save the context of the previous layer's call.
    • Tail recursion: The recursive call is the last operation before the function returns, meaning that after returning to the previous level, there is no need to continue executing other operations, so the system does not need to save the context of the previous layer's function.

    Taking the calculation of \\(1 + 2 + \\dots + n\\) as an example, we can set the result variable res as a function parameter to implement tail recursion:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
    def tail_recur(n, res):\n    \"\"\"Tail recursion\"\"\"\n    # Termination condition\n    if n == 0:\n        return res\n    # Tail recursive call\n    return tail_recur(n - 1, res + n)\n
    recursion.cpp
    /* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.java
    /* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.cs
    /* Tail recursion */\nint TailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return TailRecur(n - 1, res + n);\n}\n
    recursion.go
    /* Tail recursion */\nfunc tailRecur(n int, res int) int {\n    // Termination condition\n    if n == 0 {\n        return res\n    }\n    // Tail recursive call\n    return tailRecur(n-1, res+n)\n}\n
    recursion.swift
    /* Tail recursion */\nfunc tailRecur(n: Int, res: Int) -> Int {\n    // Termination condition\n    if n == 0 {\n        return res\n    }\n    // Tail recursive call\n    return tailRecur(n: n - 1, res: res + n)\n}\n
    recursion.js
    /* Tail recursion */\nfunction tailRecur(n, res) {\n    // Termination condition\n    if (n === 0) return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.ts
    /* Tail recursion */\nfunction tailRecur(n: number, res: number): number {\n    // Termination condition\n    if (n === 0) return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.dart
    /* Tail recursion */\nint tailRecur(int n, int res) {\n  // Termination condition\n  if (n == 0) return res;\n  // Tail recursive call\n  return tailRecur(n - 1, res + n);\n}\n
    recursion.rs
    /* Tail recursion */\nfn tail_recur(n: i32, res: i32) -> i32 {\n    // Termination condition\n    if n == 0 {\n        return res;\n    }\n    // Tail recursive call\n    tail_recur(n - 1, res + n)\n}\n
    recursion.c
    /* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
    recursion.kt
    /* Tail recursion */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n    // Add tailrec keyword to enable tail recursion optimization\n    // Termination condition\n    if (n == 0)\n        return res\n    // Tail recursive call\n    return tailRecur(n - 1, res + n)\n}\n
    recursion.rb
    ### Tail recursion ###\ndef tail_recur(n, res)\n  # Termination condition\n  return res if n == 0\n  # Tail recursive call\n  tail_recur(n - 1, res + n)\nend\n

    The execution process of tail recursion is shown in Figure 2-5. Comparing regular recursion and tail recursion, the summation operation is performed at different points.

    • Regular recursion: The summation operation is performed during the \"ascending\" process, requiring an additional summation operation after each layer returns.
    • Tail recursion: The summation operation is performed during the \"descending\" process; the \"ascending\" process only needs to return layer by layer.

    Figure 2-5   Tail recursion process

    Tip

    Please note that many compilers or interpreters do not support tail recursion optimization. For example, Python does not support tail recursion optimization by default, so even if a function is in tail recursive form, it may still encounter stack overflow issues.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3-recursion-tree","level":3,"title":"3.   Recursion Tree","text":"

    When dealing with algorithmic problems related to \"divide and conquer\", recursion often provides a more intuitive approach and more readable code than iteration. Taking the \"Fibonacci sequence\" as an example.

    Question

    Given a Fibonacci sequence \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\), find the \\(n\\)-th number in the sequence.

    Let the \\(n\\)-th number of the Fibonacci sequence be \\(f(n)\\). Two conclusions can be easily obtained.

    • The first two numbers of the sequence are \\(f(1) = 0\\) and \\(f(2) = 1\\).
    • Each number in the sequence is the sum of the previous two numbers, i.e., \\(f(n) = f(n - 1) + f(n - 2)\\).

    Following the recurrence relation to make recursive calls, with the first two numbers as termination conditions, we can write the recursive code. Calling fib(n) will give us the \\(n\\)-th number of the Fibonacci sequence:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
    def fib(n: int) -> int:\n    \"\"\"Fibonacci sequence: recursion\"\"\"\n    # Termination condition f(1) = 0, f(2) = 1\n    if n == 1 or n == 2:\n        return n - 1\n    # Recursive call f(n) = f(n-1) + f(n-2)\n    res = fib(n - 1) + fib(n - 2)\n    # Return result f(n)\n    return res\n
    recursion.cpp
    /* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.java
    /* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.cs
    /* Fibonacci sequence: recursion */\nint Fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = Fib(n - 1) + Fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.go
    /* Fibonacci sequence: recursion */\nfunc fib(n int) int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    res := fib(n-1) + fib(n-2)\n    // Return result f(n)\n    return res\n}\n
    recursion.swift
    /* Fibonacci sequence: recursion */\nfunc fib(n: Int) -> Int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    let res = fib(n: n - 1) + fib(n: n - 2)\n    // Return result f(n)\n    return res\n}\n
    recursion.js
    /* Fibonacci sequence: recursion */\nfunction fib(n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.ts
    /* Fibonacci sequence: recursion */\nfunction fib(n: number): number {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.dart
    /* Fibonacci sequence: recursion */\nint fib(int n) {\n  // Termination condition f(1) = 0, f(2) = 1\n  if (n == 1 || n == 2) return n - 1;\n  // Recursive call f(n) = f(n-1) + f(n-2)\n  int res = fib(n - 1) + fib(n - 2);\n  // Return result f(n)\n  return res;\n}\n
    recursion.rs
    /* Fibonacci sequence: recursion */\nfn fib(n: i32) -> i32 {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1;\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    let res = fib(n - 1) + fib(n - 2);\n    // Return result\n    res\n}\n
    recursion.c
    /* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
    recursion.kt
    /* Fibonacci sequence: recursion */\nfun fib(n: Int): Int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    val res = fib(n - 1) + fib(n - 2)\n    // Return result f(n)\n    return res\n}\n
    recursion.rb
    ### Fibonacci sequence: recursion ###\ndef fib(n)\n  # Termination condition f(1) = 0, f(2) = 1\n  return n - 1 if n == 1 || n == 2\n  # Recursive call f(n) = f(n-1) + f(n-2)\n  res = fib(n - 1) + fib(n - 2)\n  # Return result f(n)\n  res\nend\n

    Observing the above code, we make two recursive calls within the function, meaning that one call produces two call branches. As shown in Figure 2-6, this repeated recursive calling eventually produces a recursion tree with \\(n\\) levels.

    Figure 2-6   Recursion tree of the Fibonacci sequence

    Fundamentally, recursion embodies the paradigm of \"decomposing a problem into smaller subproblems\", and this divide-and-conquer strategy is crucial.

    • From an algorithmic perspective, many important algorithmic strategies such as searching, sorting, backtracking, divide and conquer, and dynamic programming directly or indirectly apply this way of thinking.
    • From a data structure perspective, recursion is naturally suited for handling problems related to linked lists, trees, and graphs, because they are well-suited for analysis using divide-and-conquer thinking.
    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223-comparison-of-the-two","level":2,"title":"2.2.3   Comparison of the Two","text":"

    Summarizing the above content, as shown in Table 2-1, iteration and recursion differ in implementation, performance, and applicability.

    Table 2-1   Comparison of iteration and recursion characteristics

    Iteration Recursion Implementation Loop structure Function calls itself Time efficiency Generally more efficient, no function call overhead Each function call incurs overhead Memory usage Usually uses a fixed amount of memory space Accumulated function calls may use a large amount of stack frame space Suitable problems Suitable for simple loop tasks, with intuitive and readable code Suitable for subproblem decomposition, such as trees, graphs, divide and conquer, backtracking, etc., with concise and clear code structure

    Tip

    If you find the following content difficult to understand, you can review it after reading the \"Stack\" chapter.

    What is the intrinsic relationship between iteration and recursion? Taking the above recursive function as an example, the summation operation is performed during the \"ascending\" phase of recursion. This means that the function called first actually completes its summation operation last, and this working mechanism is similar to the \"last-in, first-out\" principle of stacks.

    In fact, recursive terminology such as \"call stack\" and \"stack frame space\" already hints at the close relationship between recursion and stacks.

    1. Descend: When a function is called, the system allocates a new stack frame on the \"call stack\" for that function to store the function's local variables, parameters, return address, and other data.
    2. Ascend: When the function completes execution and returns, the corresponding stack frame is removed from the \"call stack\", restoring the execution environment of the previous function.

    Therefore, we can use an explicit stack to simulate the behavior of the call stack, thus transforming recursion into iterative form:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
    def for_loop_recur(n: int) -> int:\n    \"\"\"Simulate recursion using iteration\"\"\"\n    # Use an explicit stack to simulate the system call stack\n    stack = []\n    res = 0\n    # Recurse: recursive call\n    for i in range(n, 0, -1):\n        # Simulate \"recurse\" with \"push\"\n        stack.append(i)\n    # Return: return result\n    while stack:\n        # Simulate \"return\" with \"pop\"\n        res += stack.pop()\n    # res = 1+2+3+...+n\n    return res\n
    recursion.cpp
    /* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    stack<int> stack;\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (!stack.empty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.top();\n        stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.java
    /* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    Stack<Integer> stack = new Stack<>();\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (!stack.isEmpty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.cs
    /* Simulate recursion using iteration */\nint ForLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    Stack<int> stack = new();\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.Push(i);\n    }\n    // Return: return result\n    while (stack.Count > 0) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.Pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.go
    /* Simulate recursion using iteration */\nfunc forLoopRecur(n int) int {\n    // Use an explicit stack to simulate the system call stack\n    stack := list.New()\n    res := 0\n    // Recurse: recursive call\n    for i := n; i > 0; i-- {\n        // Simulate \"recurse\" with \"push\"\n        stack.PushBack(i)\n    }\n    // Return: return result\n    for stack.Len() != 0 {\n        // Simulate \"return\" with \"pop\"\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
    /* Simulate recursion using iteration */\nfunc forLoopRecur(n: Int) -> Int {\n    // Use an explicit stack to simulate the system call stack\n    var stack: [Int] = []\n    var res = 0\n    // Recurse: recursive call\n    for i in (1 ... n).reversed() {\n        // Simulate \"recurse\" with \"push\"\n        stack.append(i)\n    }\n    // Return: return result\n    while !stack.isEmpty {\n        // Simulate \"return\" with \"pop\"\n        res += stack.removeLast()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
    recursion.js
    /* Simulate recursion using iteration */\nfunction forLoopRecur(n) {\n    // Use an explicit stack to simulate the system call stack\n    const stack = [];\n    let res = 0;\n    // Recurse: recursive call\n    for (let i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (stack.length) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.ts
    /* Simulate recursion using iteration */\nfunction forLoopRecur(n: number): number {\n    // Use an explicit stack to simulate the system call stack\n    const stack: number[] = [];\n    let res: number = 0;\n    // Recurse: recursive call\n    for (let i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (stack.length) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.dart
    /* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n  // Use an explicit stack to simulate the system call stack\n  List<int> stack = [];\n  int res = 0;\n  // Recurse: recursive call\n  for (int i = n; i > 0; i--) {\n    // Simulate \"recurse\" with \"push\"\n    stack.add(i);\n  }\n  // Return: return result\n  while (!stack.isEmpty) {\n    // Simulate \"return\" with \"pop\"\n    res += stack.removeLast();\n  }\n  // res = 1+2+3+...+n\n  return res;\n}\n
    recursion.rs
    /* Simulate recursion using iteration */\nfn for_loop_recur(n: i32) -> i32 {\n    // Use an explicit stack to simulate the system call stack\n    let mut stack = Vec::new();\n    let mut res = 0;\n    // Recurse: recursive call\n    for i in (1..=n).rev() {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while !stack.is_empty() {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop().unwrap();\n    }\n    // res = 1+2+3+...+n\n    res\n}\n
    recursion.c
    /* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    int stack[1000]; // Use a large array to simulate stack\n    int top = -1;    // Stack top index\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack[1 + top++] = i;\n    }\n    // Return: return result\n    while (top >= 0) {\n        // Simulate \"return\" with \"pop\"\n        res += stack[top--];\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
    recursion.kt
    /* Simulate recursion using iteration */\nfun forLoopRecur(n: Int): Int {\n    // Use an explicit stack to simulate the system call stack\n    val stack = Stack<Int>()\n    var res = 0\n    // Descend: recursive call\n    for (i in n downTo 0) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i)\n    }\n    // Return: return result\n    while (stack.isNotEmpty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
    recursion.rb
    ### Use iteration to simulate recursion ###\ndef for_loop_recur(n)\n  # Use an explicit stack to simulate the system call stack\n  stack = []\n  res = 0\n\n  # Recurse: recursive call\n  for i in n.downto(0)\n    # Simulate \"recurse\" with \"push\"\n    stack << i\n  end\n  # Return: return result\n  while !stack.empty?\n    res += stack.pop\n  end\n\n  # res = 1+2+3+...+n\n  res\nend\n

    Observing the above code, when recursion is transformed into iteration, the code becomes more complex. Although iteration and recursion can be converted into each other in many cases, it may not be worthwhile to do so for the following two reasons.

    • The transformed code may be more difficult to understand and less readable.
    • For some complex problems, simulating the behavior of the system call stack can be very difficult.

    In summary, choosing between iteration and recursion depends on the nature of the specific problem. In programming practice, it is crucial to weigh the pros and cons of both and choose the appropriate method based on the context.

    ","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   Algorithm Efficiency Evaluation","text":"

    In algorithm design, we pursue the following two levels of objectives sequentially.

    1. Finding a solution to the problem: The algorithm must reliably obtain the correct solution within the specified input range.
    2. Seeking the optimal solution: Multiple solutions may exist for the same problem, and we hope to find an algorithm that is as efficient as possible.

    In other words, under the premise of being able to solve the problem, algorithm efficiency has become the primary evaluation criterion for measuring the quality of algorithms. It includes the following two dimensions.

    • Time efficiency: The length of time the algorithm runs.
    • Space efficiency: The size of memory space the algorithm occupies.

    In short, our goal is to design data structures and algorithms that are \"both fast and memory-efficient\". Effectively evaluating algorithm efficiency is crucial, because only in this way can we compare various algorithms and guide the algorithm design and optimization process.

    Efficiency evaluation methods are mainly divided into two types: actual testing and theoretical estimation.

    ","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211-actual-testing","level":2,"title":"2.1.1   Actual Testing","text":"

    Suppose we now have algorithm A and algorithm B, both of which can solve the same problem, and we need to compare their efficiency. The most direct method is to run them on a computer and measure their running time and memory usage. This evaluation approach can reflect real-world behavior, but it also has considerable limitations.

    On one hand, it is difficult to eliminate interference factors from the testing environment. Hardware configuration affects algorithmic performance. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm performs memory-intensive operations, it will benefit more from high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical.

    On the other hand, conducting complete testing is very resource-intensive. As the input data volume changes, the algorithm will exhibit different efficiencies. For example, when the input data volume is small, the running time of algorithm A is shorter than algorithm B; but when the input data volume is large, the test results may be exactly the opposite. Therefore, to obtain convincing conclusions, we need to test input data of various scales, which requires a large amount of computational resources.

    ","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212-theoretical-estimation","level":2,"title":"2.1.2   Theoretical Estimation","text":"

    Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through theoretical calculation. This estimation method is called asymptotic complexity analysis, or complexity analysis for short.

    Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. It describes the growth trend of the time and space required for algorithm execution as the input data scale increases. This definition is a bit cumbersome, so we can break it down into three key points to understand.

    • \"Time and space resources\" correspond to time complexity and space complexity, respectively.
    • \"As the input data scale increases\" means that complexity reflects the relationship between algorithm running efficiency and input data scale.
    • \"Growth trend of time and space\" indicates that complexity analysis focuses not on the specific values of running time or occupied space, but on how \"fast\" time or space grows.

    Complexity analysis overcomes the drawbacks of the actual testing method, reflected in the following aspects.

    • It does not need to actually run the code, making it more environmentally friendly and energy-efficient.
    • It is independent of the testing environment, and the analysis results are applicable to all running platforms.
    • It can reflect algorithm efficiency at different data volumes, especially algorithm performance at large data volumes.

    Tip

    If you are still confused about the concept of complexity, don't worry—we will introduce it in detail in subsequent chapters.

    Complexity analysis provides us with a \"ruler\" for evaluating algorithm efficiency, allowing us to measure the time and space resources required to execute a certain algorithm and compare the efficiency between different algorithms.

    Complexity is a mathematical concept that may feel abstract and challenging for beginners. From this perspective, complexity analysis may not be the most suitable topic to introduce first. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage.

    In summary, it is recommended that before diving deep into data structures and algorithms, you first establish a preliminary understanding of complexity analysis so that you can analyze the complexity of simple algorithms.

    ","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   Space Complexity","text":"

    Space complexity measures the growth trend of memory space occupied by an algorithm as the data size increases. This concept is very similar to time complexity, except that \"running time\" is replaced with \"occupied memory space\".

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241-algorithm-related-space","level":2,"title":"2.4.1   Algorithm-Related Space","text":"

    The memory space used by an algorithm during execution mainly includes the following types.

    • Input space: Used to store the input data of the algorithm.
    • Temporary space: Used to store variables, objects, function contexts, and other data during the algorithm's execution.
    • Output space: Used to store the output data of the algorithm.

    In general, the scope of space complexity statistics is \"temporary space\" plus \"output space\".

    Temporary space can be further divided into three parts.

    • Temporary data: Used to save various constants, variables, objects, etc., during the algorithm's execution.
    • Stack frame space: Used to save the context data of called functions. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns.
    • Instruction space: Used to save compiled program instructions, which are usually ignored in actual statistics.

    When analyzing the space complexity of a program, we usually consider three parts: temporary data, stack frame space, and output data, as shown in the following figure.

    Figure 2-15   Algorithm-related space

    The related code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class Node:\n    \"\"\"Class\"\"\"\n    def __init__(self, x: int):\n        self.val: int = x              # Node value\n        self.next: Node | None = None  # Reference to the next node\n\ndef function() -> int:\n    \"\"\"Function\"\"\"\n    # Perform some operations...\n    return 0\n\ndef algorithm(n) -> int:  # Input data\n    A = 0                 # Temporary data (constant, usually represented by uppercase letters)\n    b = 0                 # Temporary data (variable)\n    node = Node(0)        # Temporary data (object)\n    c = function()        # Stack frame space (function call)\n    return A + b + c      # Output data\n
    /* Structure */\nstruct Node {\n    int val;\n    Node *next;\n    Node(int x) : val(x), next(nullptr) {}\n};\n\n/* Function */\nint func() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) {        // Input data\n    const int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node* node = new Node(0); // Temporary data (object)\n    int c = func();           // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
    /* Class */\nclass Node {\n    int val;\n    Node next;\n    Node(int x) { val = x; }\n}\n\n/* Function */\nint function() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) {        // Input data\n    final int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node node = new Node(0);  // Temporary data (object)\n    int c = function();       // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
    /* Class */\nclass Node(int x) {\n    int val = x;\n    Node next;\n}\n\n/* Function */\nint Function() {\n    // Perform some operations...\n    return 0;\n}\n\nint Algorithm(int n) {        // Input data\n    const int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node node = new(0);       // Temporary data (object)\n    int c = Function();       // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
    /* Structure */\ntype node struct {\n    val  int\n    next *node\n}\n\n/* Create node structure */\nfunc newNode(val int) *node {\n    return &node{val: val}\n}\n\n/* Function */\nfunc function() int {\n    // Perform some operations...\n    return 0\n}\n\nfunc algorithm(n int) int { // Input data\n    const a = 0             // Temporary data (constant)\n    b := 0                  // Temporary data (variable)\n    newNode(0)              // Temporary data (object)\n    c := function()         // Stack frame space (function call)\n    return a + b + c        // Output data\n}\n
    /* Class */\nclass Node {\n    var val: Int\n    var next: Node?\n\n    init(x: Int) {\n        val = x\n    }\n}\n\n/* Function */\nfunc function() -> Int {\n    // Perform some operations...\n    return 0\n}\n\nfunc algorithm(n: Int) -> Int { // Input data\n    let a = 0             // Temporary data (constant)\n    var b = 0             // Temporary data (variable)\n    let node = Node(x: 0) // Temporary data (object)\n    let c = function()    // Stack frame space (function call)\n    return a + b + c      // Output data\n}\n
    /* Class */\nclass Node {\n    val;\n    next;\n    constructor(val) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.next = null;                       // Reference to the next node\n    }\n}\n\n/* Function */\nfunction constFunc() {\n    // Perform some operations\n    return 0;\n}\n\nfunction algorithm(n) {       // Input data\n    const a = 0;              // Temporary data (constant)\n    let b = 0;                // Temporary data (variable)\n    const node = new Node(0); // Temporary data (object)\n    const c = constFunc();    // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
    /* Class */\nclass Node {\n    val: number;\n    next: Node | null;\n    constructor(val?: number) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.next = null;                       // Reference to the next node\n    }\n}\n\n/* Function */\nfunction constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n\nfunction algorithm(n: number): number { // Input data\n    const a = 0;                        // Temporary data (constant)\n    let b = 0;                          // Temporary data (variable)\n    const node = new Node(0);           // Temporary data (object)\n    const c = constFunc();              // Stack frame space (function call)\n    return a + b + c;                   // Output data\n}\n
    /* Class */\nclass Node {\n  int val;\n  Node next;\n  Node(this.val, [this.next]);\n}\n\n/* Function */\nint function() {\n  // Perform some operations...\n  return 0;\n}\n\nint algorithm(int n) {  // Input data\n  const int a = 0;      // Temporary data (constant)\n  int b = 0;            // Temporary data (variable)\n  Node node = Node(0);  // Temporary data (object)\n  int c = function();   // Stack frame space (function call)\n  return a + b + c;     // Output data\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Structure */\nstruct Node {\n    val: i32,\n    next: Option<Rc<RefCell<Node>>>,\n}\n\n/* Create Node structure */\nimpl Node {\n    fn new(val: i32) -> Self {\n        Self { val: val, next: None }\n    }\n}\n\n/* Function */\nfn function() -> i32 {\n    // Perform some operations...\n    return 0;\n}\n\nfn algorithm(n: i32) -> i32 {       // Input data\n    const a: i32 = 0;               // Temporary data (constant)\n    let mut b = 0;                  // Temporary data (variable)\n    let node = Node::new(0);        // Temporary data (object)\n    let c = function();             // Stack frame space (function call)\n    return a + b + c;               // Output data\n}\n
    /* Function */\nint func() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) { // Input data\n    const int a = 0;   // Temporary data (constant)\n    int b = 0;         // Temporary data (variable)\n    int c = func();    // Stack frame space (function call)\n    return a + b + c;  // Output data\n}\n
    /* Class */\nclass Node(var _val: Int) {\n    var next: Node? = null\n}\n\n/* Function */\nfun function(): Int {\n    // Perform some operations...\n    return 0\n}\n\nfun algorithm(n: Int): Int { // Input data\n    val a = 0                // Temporary data (constant)\n    var b = 0                // Temporary data (variable)\n    val node = Node(0)       // Temporary data (object)\n    val c = function()       // Stack frame space (function call)\n    return a + b + c         // Output data\n}\n
    ### Class ###\nclass Node\n    attr_accessor :val      # Node value\n    attr_accessor :next     # Reference to the next node\n\n    def initialize(x)\n        @val = x\n    end\nend\n\n### Function ###\ndef function\n    # Perform some operations...\n    0\nend\n\n### Algorithm ###\ndef algorithm(n)        # Input data\n    a = 0               # Temporary data (constant)\n    b = 0               # Temporary data (variable)\n    node = Node.new(0)  # Temporary data (object)\n    c = function        # Stack frame space (function call)\n    a + b + c           # Output data\nend\n
    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242-calculation-method","level":2,"title":"2.4.2   Calculation Method","text":"

    The calculation method for space complexity is roughly the same as for time complexity, except that what we measure changes from the \"number of operations\" to the \"amount of space used\".

    Unlike time complexity, we usually only focus on the worst-case space complexity. This is because memory space is a hard requirement, and we must ensure that sufficient memory space is reserved for all input data.

    Observe the following code. Here, \"worst case\" in worst-case space complexity has two meanings.

    1. Based on the worst input data: When \\(n < 10\\), the space complexity is \\(O(1)\\); but when \\(n > 10\\), the initialized array nums occupies \\(O(n)\\) space, so the worst-case space complexity is \\(O(n)\\).
    2. Based on the peak memory during algorithm execution: For example, before executing the last line, the program occupies \\(O(1)\\) space; when initializing the array nums, the program occupies \\(O(n)\\) space, so the worst-case space complexity is \\(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

    In recursive functions, it is necessary to count the stack frame space. Observe the following code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    def function() -> int:\n    # Perform some operations\n    return 0\n\ndef loop(n: int):\n    \"\"\"Loop has space complexity of O(1)\"\"\"\n    for _ in range(n):\n        function()\n\ndef recur(n: int):\n    \"\"\"Recursion has space complexity of O(n)\"\"\"\n    if n == 1:\n        return\n    return recur(n - 1)\n
    int func() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
    int function() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
    int Function() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid Loop(int n) {\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nint Recur(int n) {\n    if (n == 1) return 1;\n    return Recur(n - 1);\n}\n
    func function() int {\n    // Perform some operations\n    return 0\n}\n\n/* Loop has space complexity of O(1) */\nfunc loop(n int) {\n    for i := 0; i < n; i++ {\n        function()\n    }\n}\n\n/* Recursion has space complexity of O(n) */\nfunc recur(n int) {\n    if n == 1 {\n        return\n    }\n    recur(n - 1)\n}\n
    @discardableResult\nfunc function() -> Int {\n    // Perform some operations\n    return 0\n}\n\n/* Loop has space complexity of O(1) */\nfunc loop(n: Int) {\n    for _ in 0 ..< n {\n        function()\n    }\n}\n\n/* Recursion has space complexity of O(n) */\nfunc recur(n: Int) {\n    if n == 1 {\n        return\n    }\n    recur(n: n - 1)\n}\n
    function constFunc() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfunction loop(n) {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfunction recur(n) {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
    function constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfunction loop(n: number): void {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfunction recur(n: number): void {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
    int function() {\n  // Perform some operations\n  return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n  for (int i = 0; i < n; i++) {\n    function();\n  }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n  if (n == 1) return;\n  recur(n - 1);\n}\n
    fn function() -> i32 {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfn loop(n: i32) {\n    for i in 0..n {\n        function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfn recur(n: i32) {\n    if n == 1 {\n        return;\n    }\n    recur(n - 1);\n}\n
    int func() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
    fun function(): Int {\n    // Perform some operations\n    return 0\n}\n/* Loop has space complexity of O(1) */\nfun loop(n: Int) {\n    for (i in 0..<n) {\n        function()\n    }\n}\n/* Recursion has space complexity of O(n) */\nfun recur(n: Int) {\n    if (n == 1) return\n    return recur(n - 1)\n}\n
    def function\n    # Perform some operations\n    0\nend\n\n### Loop has space complexity of O(1) ###\ndef loop(n)\n    (0...n).each { function }\nend\n\n### Recursion has space complexity of O(n) ###\ndef recur(n)\n    return if n == 1\n    recur(n - 1)\nend\n

    The time complexity of both functions loop() and recur() is \\(O(n)\\), but their space complexities are different.

    • The function loop() calls function() \\(n\\) times in a loop. In each iteration, function() returns and releases its stack frame space, so the space complexity remains \\(O(1)\\).
    • The recursive function recur() has \\(n\\) unreturned recur() instances existing simultaneously during execution, thus occupying \\(O(n)\\) stack frame space.
    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243-common-types","level":2,"title":"2.4.3   Common Types","text":"

    Let the input data size be \\(n\\). The following figure shows common types of space complexity (arranged from low to high).

    \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline & \\text{Constant} < \\text{Logarithmic} < \\text{Linear} < \\text{Quadratic} < \\text{Exponential} \\end{aligned} \\]

    Figure 2-16   Common types of space complexity

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-constant-order-o1","level":3,"title":"1.   Constant Order \\(O(1)\\)","text":"

    Constant order is common for constants, variables, and objects whose number is independent of the input data size \\(n\\).

    It should be noted that memory occupied by initializing variables or calling functions in a loop is released when entering the next iteration, so it does not accumulate space, and the space complexity remains \\(O(1)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def function() -> int:\n    \"\"\"Function\"\"\"\n    # Perform some operations\n    return 0\n\ndef constant(n: int):\n    \"\"\"Constant order\"\"\"\n    # Constants, variables, objects occupy O(1) space\n    a = 0\n    nums = [0] * 10000\n    node = ListNode(0)\n    # Variables in the loop occupy O(1) space\n    for _ in range(n):\n        c = 0\n    # Functions in the loop occupy O(1) space\n    for _ in range(n):\n        function()\n
    space_complexity.cpp
    /* Function */\nint func() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    const int a = 0;\n    int b = 0;\n    vector<int> nums(10000);\n    ListNode node(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
    space_complexity.java
    /* Function */\nint function() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    final int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n
    space_complexity.cs
    /* Function */\nint Function() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid Constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n
    space_complexity.go
    /* Function */\nfunc function() int {\n    // Perform some operations...\n    return 0\n}\n\n/* Constant order */\nfunc spaceConstant(n int) {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0\n    b := 0\n    nums := make([]int, 10000)\n    node := newNode(0)\n    // Variables in the loop occupy O(1) space\n    var c int\n    for i := 0; i < n; i++ {\n        c = 0\n    }\n    // Functions in the loop occupy O(1) space\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
    /* Function */\n@discardableResult\nfunc function() -> Int {\n    // Perform some operations\n    return 0\n}\n\n/* Constant order */\nfunc constant(n: Int) {\n    // Constants, variables, objects occupy O(1) space\n    let a = 0\n    var b = 0\n    let nums = Array(repeating: 0, count: 10000)\n    let node = ListNode(x: 0)\n    // Variables in the loop occupy O(1) space\n    for _ in 0 ..< n {\n        let c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for _ in 0 ..< n {\n        function()\n    }\n}\n
    space_complexity.js
    /* Function */\nfunction constFunc() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nfunction constant(n) {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
    space_complexity.ts
    /* Function */\nfunction constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nfunction constant(n: number): void {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
    space_complexity.dart
    /* Function */\nint function() {\n  // Perform some operations\n  return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n  // Constants, variables, objects occupy O(1) space\n  final int a = 0;\n  int b = 0;\n  List<int> nums = List.filled(10000, 0);\n  ListNode node = ListNode(0);\n  // Variables in the loop occupy O(1) space\n  for (var i = 0; i < n; i++) {\n    int c = 0;\n  }\n  // Functions in the loop occupy O(1) space\n  for (var i = 0; i < n; i++) {\n    function();\n  }\n}\n
    space_complexity.rs
    /* Function */\nfn function() -> i32 {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\n#[allow(unused)]\nfn constant(n: i32) {\n    // Constants, variables, objects occupy O(1) space\n    const A: i32 = 0;\n    let b = 0;\n    let nums = vec![0; 10000];\n    let node = ListNode::new(0);\n    // Variables in the loop occupy O(1) space\n    for i in 0..n {\n        let c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for i in 0..n {\n        function();\n    }\n}\n
    space_complexity.c
    /* Function */\nint func() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    const int a = 0;\n    int b = 0;\n    int nums[1000];\n    ListNode *node = newListNode(0);\n    free(node);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
    space_complexity.kt
    /* Function */\nfun function(): Int {\n    // Perform some operations\n    return 0\n}\n\n/* Constant order */\nfun constant(n: Int) {\n    // Constants, variables, objects occupy O(1) space\n    val a = 0\n    var b = 0\n    val nums = Array(10000) { 0 }\n    val node = ListNode(0)\n    // Variables in the loop occupy O(1) space\n    for (i in 0..<n) {\n        val c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for (i in 0..<n) {\n        function()\n    }\n}\n
    space_complexity.rb
    ### Function ###\ndef function\n  # Perform some operations\n  0\nend\n\n### Constant time ###\ndef constant(n)\n  # Constants, variables, objects occupy O(1) space\n  a = 0\n  nums = [0] * 10000\n  node = ListNode.new\n\n  # Variables in the loop occupy O(1) space\n  (0...n).each { c = 0 }\n  # Functions in the loop occupy O(1) space\n  (0...n).each { function }\nend\n
    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-linear-order-on","level":3,"title":"2.   Linear Order \\(O(n)\\)","text":"

    Linear order is common in arrays, linked lists, stacks, queues, etc., where the number of elements is proportional to \\(n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def linear(n: int):\n    \"\"\"Linear order\"\"\"\n    # A list of length n occupies O(n) space\n    nums = [0] * n\n    # A hash table of length n occupies O(n) space\n    hmap = dict[int, str]()\n    for i in range(n):\n        hmap[i] = str(i)\n
    space_complexity.cpp
    /* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    vector<int> nums(n);\n    // A list of length n occupies O(n) space\n    vector<ListNode> nodes;\n    for (int i = 0; i < n; i++) {\n        nodes.push_back(ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    int[] nums = new int[n];\n    // A list of length n occupies O(n) space\n    List<ListNode> nodes = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        nodes.add(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nvoid Linear(int n) {\n    // Array of length n uses O(n) space\n    int[] nums = new int[n];\n    // A list of length n occupies O(n) space\n    List<ListNode> nodes = [];\n    for (int i = 0; i < n; i++) {\n        nodes.Add(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nfunc spaceLinear(n int) {\n    // Array of length n uses O(n) space\n    _ = make([]int, n)\n    // A list of length n occupies O(n) space\n    var nodes []*node\n    for i := 0; i < n; i++ {\n        nodes = append(nodes, newNode(i))\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nfunc linear(n: Int) {\n    // Array of length n uses O(n) space\n    let nums = Array(repeating: 0, count: n)\n    // A list of length n occupies O(n) space\n    let nodes = (0 ..< n).map { ListNode(x: $0) }\n    // A hash table of length n occupies O(n) space\n    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, \"\\($0)\") })\n}\n
    space_complexity.js
    /* Linear order */\nfunction linear(n) {\n    // Array of length n uses O(n) space\n    const nums = new Array(n);\n    // A list of length n occupies O(n) space\n    const nodes = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nfunction linear(n: number): void {\n    // Array of length n uses O(n) space\n    const nums = new Array(n);\n    // A list of length n occupies O(n) space\n    const nodes: ListNode[] = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Linear order */\nvoid linear(int n) {\n  // Array of length n uses O(n) space\n  List<int> nums = List.filled(n, 0);\n  // A list of length n occupies O(n) space\n  List<ListNode> nodes = [];\n  for (var i = 0; i < n; i++) {\n    nodes.add(ListNode(i));\n  }\n  // A hash table of length n occupies O(n) space\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
    /* Linear order */\n#[allow(unused)]\nfn linear(n: i32) {\n    // Array of length n uses O(n) space\n    let mut nums = vec![0; n as usize];\n    // A list of length n occupies O(n) space\n    let mut nodes = Vec::new();\n    for i in 0..n {\n        nodes.push(ListNode::new(i))\n    }\n    // A hash table of length n occupies O(n) space\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
    /* Hash table */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Implemented using uthash.h\n} HashTable;\n\n/* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    int *nums = malloc(sizeof(int) * n);\n    free(nums);\n\n    // A list of length n occupies O(n) space\n    ListNode **nodes = malloc(sizeof(ListNode *) * n);\n    for (int i = 0; i < n; i++) {\n        nodes[i] = newListNode(i);\n    }\n    // Memory release\n    for (int i = 0; i < n; i++) {\n        free(nodes[i]);\n    }\n    free(nodes);\n\n    // A hash table of length n occupies O(n) space\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    // Memory release\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
    /* Linear order */\nfun linear(n: Int) {\n    // Array of length n uses O(n) space\n    val nums = Array(n) { 0 }\n    // A list of length n occupies O(n) space\n    val nodes = mutableListOf<ListNode>()\n    for (i in 0..<n) {\n        nodes.add(ListNode(i))\n    }\n    // A hash table of length n occupies O(n) space\n    val map = mutableMapOf<Int, String>()\n    for (i in 0..<n) {\n        map[i] = i.toString()\n    }\n}\n
    space_complexity.rb
    ### Linear time ###\ndef linear(n)\n  # A list of length n occupies O(n) space\n  nums = Array.new(n, 0)\n\n  # A hash table of length n occupies O(n) space\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n

    As shown in the following figure, the recursion depth of this function is \\(n\\), meaning that there are \\(n\\) unreturned linear_recur() functions existing simultaneously, using \\(O(n)\\) stack frame space:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def linear_recur(n: int):\n    \"\"\"Linear order (recursive implementation)\"\"\"\n    print(\"Recursion n =\", n)\n    if n == 1:\n        return\n    linear_recur(n - 1)\n
    space_complexity.cpp
    /* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    cout << \"Recursion n = \" << n << endl;\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
    space_complexity.java
    /* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    System.out.println(\"Recursion n = \" + n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
    space_complexity.cs
    /* Linear order (recursive implementation) */\nvoid LinearRecur(int n) {\n    Console.WriteLine(\"Recursion n = \" + n);\n    if (n == 1) return;\n    LinearRecur(n - 1);\n}\n
    space_complexity.go
    /* Linear order (recursive implementation) */\nfunc spaceLinearRecur(n int) {\n    fmt.Println(\"Recursion n =\", n)\n    if n == 1 {\n        return\n    }\n    spaceLinearRecur(n - 1)\n}\n
    space_complexity.swift
    /* Linear order (recursive implementation) */\nfunc linearRecur(n: Int) {\n    print(\"Recursion n = \\(n)\")\n    if n == 1 {\n        return\n    }\n    linearRecur(n: n - 1)\n}\n
    space_complexity.js
    /* Linear order (recursive implementation) */\nfunction linearRecur(n) {\n    console.log(`Recursion n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
    space_complexity.ts
    /* Linear order (recursive implementation) */\nfunction linearRecur(n: number): void {\n    console.log(`Recursion n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
    space_complexity.dart
    /* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n  print('Recursion n = $n');\n  if (n == 1) return;\n  linearRecur(n - 1);\n}\n
    space_complexity.rs
    /* Linear order (recursive implementation) */\nfn linear_recur(n: i32) {\n    println!(\"Recursion n = {}\", n);\n    if n == 1 {\n        return;\n    };\n    linear_recur(n - 1);\n}\n
    space_complexity.c
    /* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    printf(\"Recursion n = %d\\r\\n\", n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
    space_complexity.kt
    /* Linear order (recursive implementation) */\nfun linearRecur(n: Int) {\n    println(\"Recursion n = $n\")\n    if (n == 1)\n        return\n    linearRecur(n - 1)\n}\n
    space_complexity.rb
    ### Linear space (recursive) ###\ndef linear_recur(n)\n  puts \"Recursion n = #{n}\"\n  return if n == 1\n  linear_recur(n - 1)\nend\n

    Figure 2-17   Linear order space complexity generated by recursive function

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-quadratic-order-on2","level":3,"title":"3.   Quadratic Order \\(O(n^2)\\)","text":"

    Quadratic order is common in matrices and graphs, where the number of elements is quadratically related to \\(n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def quadratic(n: int):\n    \"\"\"Quadratic order\"\"\"\n    # A 2D list occupies O(n^2) space\n    num_matrix = [[0] * n for _ in range(n)]\n
    space_complexity.cpp
    /* Exponential order */\nvoid quadratic(int n) {\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nvoid quadratic(int n) {\n    // Matrix uses O(n^2) space\n    int[][] numMatrix = new int[n][n];\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nvoid Quadratic(int n) {\n    // Matrix uses O(n^2) space\n    int[,] numMatrix = new int[n, n];\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nfunc spaceQuadratic(n int) {\n    // Matrix uses O(n^2) space\n    numMatrix := make([][]int, n)\n    for i := 0; i < n; i++ {\n        numMatrix[i] = make([]int, n)\n    }\n}\n
    space_complexity.swift
    /* Exponential order */\nfunc quadratic(n: Int) {\n    // 2D list uses O(n^2) space\n    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n
    space_complexity.js
    /* Exponential order */\nfunction quadratic(n) {\n    // Matrix uses O(n^2) space\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nfunction quadratic(n: number): void {\n    // Matrix uses O(n^2) space\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nvoid quadratic(int n) {\n  // Matrix uses O(n^2) space\n  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));\n  // 2D list uses O(n^2) space\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
    /* Exponential order */\n#[allow(unused)]\nfn quadratic(n: i32) {\n    // Matrix uses O(n^2) space\n    let num_matrix = vec![vec![0; n as usize]; n as usize];\n    // 2D list uses O(n^2) space\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
    /* Exponential order */\nvoid quadratic(int n) {\n    // 2D list uses O(n^2) space\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    // Memory release\n    for (int i = 0; i < n; i++) {\n        free(numMatrix[i]);\n    }\n    free(numMatrix);\n}\n
    space_complexity.kt
    /* Exponential order */\nfun quadratic(n: Int) {\n    // Matrix uses O(n^2) space\n    val numMatrix = arrayOfNulls<Array<Int>?>(n)\n    // 2D list uses O(n^2) space\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
    ### Quadratic time ###\ndef quadratic(n)\n  # 2D list uses O(n^2) space\n  Array.new(n) { Array.new(n, 0) }\nend\n

    As shown in the following figure, the recursion depth of this function is \\(n\\), and an array is initialized in each recursive function with lengths of \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\), with an average length of \\(n / 2\\), thus occupying \\(O(n^2)\\) space overall:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def quadratic_recur(n: int) -> int:\n    \"\"\"Quadratic order (recursive implementation)\"\"\"\n    if n <= 0:\n        return 0\n    # Array nums length is n, n-1, ..., 2, 1\n    nums = [0] * n\n    return quadratic_recur(n - 1)\n
    space_complexity.cpp
    /* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    vector<int> nums(n);\n    cout << \"In recursion n = \" << n << \", nums length = \" << nums.size() << endl;\n    return quadraticRecur(n - 1);\n}\n
    space_complexity.java
    /* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    // Array nums has length n, n-1, ..., 2, 1\n    int[] nums = new int[n];\n    System.out.println(\"In recursion n = \" + n + \", nums length = \" + nums.length);\n    return quadraticRecur(n - 1);\n}\n
    space_complexity.cs
    /* Quadratic order (recursive implementation) */\nint QuadraticRecur(int n) {\n    if (n <= 0) return 0;\n    int[] nums = new int[n];\n    Console.WriteLine(\"Recursion n = \" + n + \", nums length = \" + nums.Length);\n    return QuadraticRecur(n - 1);\n}\n
    space_complexity.go
    /* Quadratic order (recursive implementation) */\nfunc spaceQuadraticRecur(n int) int {\n    if n <= 0 {\n        return 0\n    }\n    nums := make([]int, n)\n    fmt.Printf(\"In recursion n = %d, nums length = %d \\n\", n, len(nums))\n    return spaceQuadraticRecur(n - 1)\n}\n
    space_complexity.swift
    /* Quadratic order (recursive implementation) */\n@discardableResult\nfunc quadraticRecur(n: Int) -> Int {\n    if n <= 0 {\n        return 0\n    }\n    // Array nums has length n, n-1, ..., 2, 1\n    let nums = Array(repeating: 0, count: n)\n    print(\"In recursion n = \\(n), nums length = \\(nums.count)\")\n    return quadraticRecur(n: n - 1)\n}\n
    space_complexity.js
    /* Quadratic order (recursive implementation) */\nfunction quadraticRecur(n) {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
    space_complexity.ts
    /* Quadratic order (recursive implementation) */\nfunction quadraticRecur(n: number): number {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
    space_complexity.dart
    /* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n  if (n <= 0) return 0;\n  List<int> nums = List.filled(n, 0);\n  print('In recursion n = $n, nums length = ${nums.length}');\n  return quadraticRecur(n - 1);\n}\n
    space_complexity.rs
    /* Quadratic order (recursive implementation) */\nfn quadratic_recur(n: i32) -> i32 {\n    if n <= 0 {\n        return 0;\n    };\n    // Array nums has length n, n-1, ..., 2, 1\n    let nums = vec![0; n as usize];\n    println!(\"In recursion n = {}, nums length = {}\", n, nums.len());\n    return quadratic_recur(n - 1);\n}\n
    space_complexity.c
    /* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    int *nums = malloc(sizeof(int) * n);\n    printf(\"In recursion n = %d, nums length = %d\\r\\n\", n, n);\n    int res = quadraticRecur(n - 1);\n    free(nums);\n    return res;\n}\n
    space_complexity.kt
    /* Quadratic order (recursive implementation) */\ntailrec fun quadraticRecur(n: Int): Int {\n    if (n <= 0)\n        return 0\n    // Array nums has length n, n-1, ..., 2, 1\n    val nums = Array(n) { 0 }\n    println(\"In recursion n = $n, nums length = ${nums.size}\")\n    return quadraticRecur(n - 1)\n}\n
    space_complexity.rb
    ### Quadratic space (recursive) ###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # Array nums has length n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n

    Figure 2-18   Quadratic order space complexity generated by recursive function

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-exponential-order-o2n","level":3,"title":"4.   Exponential Order \\(O(2^n)\\)","text":"

    Exponential order is common in binary trees. Observe the following figure: a \"full binary tree\" with \\(n\\) levels has \\(2^n - 1\\) nodes, occupying \\(O(2^n)\\) space:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
    def build_tree(n: int) -> TreeNode | None:\n    \"\"\"Exponential order (build full binary tree)\"\"\"\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    /* Driver Code */\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
    ### Exponential space (build full binary tree) ###\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

    Figure 2-19   Exponential order space complexity generated by full binary tree

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-logarithmic-order-olog-n","level":3,"title":"5.   Logarithmic Order \\(O(\\log n)\\)","text":"

    Logarithmic order is common in divide-and-conquer algorithms. For example, merge sort: given an input array of length \\(n\\), each recursion divides the array in half from the midpoint, forming a recursion tree of height \\(\\log n\\), using \\(O(\\log n)\\) stack frame space.

    Another example is converting a number to a string. Given a positive integer \\(n\\), it has \\(\\lfloor \\log_{10} n \\rfloor + 1\\) digits, i.e., the corresponding string length is \\(\\lfloor \\log_{10} n \\rfloor + 1\\), so the space complexity is \\(O(\\log_{10} n + 1) = O(\\log n)\\).

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244-trading-time-for-space","level":2,"title":"2.4.4   Trading Time for Space","text":"

    Ideally, we hope that both the time complexity and space complexity of an algorithm can reach optimal. However, in practice, optimizing both time complexity and space complexity simultaneously is usually very difficult.

    Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa. Sacrificing memory space to improve execution speed is called \"trading space for time\"; the reverse is called \"trading time for space\".

    The choice of which approach depends on which aspect we value more. In most cases, time is more precious than space, so \"trading space for time\" is usually the more common strategy. Of course, when the data volume is very large, controlling space complexity is also very important.

    ","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   Summary","text":"","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"

    Algorithm Efficiency Assessment

    • Time efficiency and space efficiency are the two primary evaluation metrics for measuring algorithm performance.
    • We can evaluate algorithm efficiency through actual testing, but it is difficult to eliminate the influence of the testing environment, and it consumes substantial computational resources.
    • Complexity analysis can overcome the limitations of actual testing. Its results apply across running platforms, and it can reveal algorithm efficiency under different data scales.

    Time Complexity

    • Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but it may be less informative in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency.
    • Worst-case time complexity is represented using Big \\(O\\) notation, corresponding to the asymptotic upper bound of a function, reflecting the growth level of the number of operations \\(T(n)\\) as \\(n\\) approaches positive infinity.
    • Deriving time complexity involves two steps: first, counting the number of operations, then determining the asymptotic upper bound.
    • Common time complexities arranged from low to high include \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n \\log n)\\), \\(O(n^2)\\), \\(O(2^n)\\), and \\(O(n!)\\).
    • The time complexity of some algorithms is not fixed, but rather depends on the distribution of input data. Time complexity is divided into worst-case, best-case, and average-case time complexity. Best-case time complexity is rarely used because input data generally needs to satisfy strict conditions to achieve the best case.
    • Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires analyzing the input data distribution and the resulting mathematical expectation.

    Space Complexity

    • Space complexity serves a similar purpose to time complexity, used to measure the trend of algorithm memory usage as data volume increases.
    • The memory space related to algorithm execution can be divided into input space, temporary space, and output space. Typically, input space is not included in space complexity calculations. Temporary space can be divided into temporary data, stack frame space, and instruction space, where stack frame space usually affects space complexity only in recursive functions.
    • We typically only focus on worst-case space complexity, which is the space complexity of an algorithm under worst-case input data and worst-case runtime.
    • Common space complexities arranged from low to high include \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n^2)\\), and \\(O(2^n)\\).
    ","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Is the space complexity of tail recursion \\(O(1)\\)?

    Theoretically, the space complexity of tail recursive functions can be optimized to \\(O(1)\\). However, most programming languages (such as Java, Python, C++, Go, C#, etc.) do not support automatic tail recursion optimization, so the space complexity is generally considered to be \\(O(n)\\).

    Q: What is the difference between the terms function and method?

    A function can be executed independently, with all parameters passed explicitly. A method is associated with an object, is implicitly bound to the object that invokes it, and can operate on data contained in class instances.

    The following examples use several common programming languages for illustration.

    • C is a procedural programming language without object-oriented concepts, so it only has functions. However, we can simulate object-oriented programming by creating structures (struct), and functions associated with structures are equivalent to methods in other programming languages.
    • Java and C# are object-oriented programming languages where code blocks (methods) are typically part of a class. Static methods behave like functions because they are bound to the class and cannot access specific instance variables.
    • C++ and Python support both procedural programming (functions) and object-oriented programming (methods).

    Q: Does the diagram for \"common space complexity types\" reflect the absolute size of occupied space?

    No, the diagram shows space complexity, which reflects growth trends rather than the absolute size of occupied space.

    Assuming \\(n = 8\\), you might find that the values of each curve do not correspond to the functions. This is because each curve contains a constant term used to compress the value range into a visually comfortable range.

    In practice, because we generally do not know the \"constant-term\" cost of each method, we usually cannot choose the optimal solution for cases like \\(n = 8\\) based on complexity alone. But for \\(n = 8^5\\), the choice is straightforward, because the growth trend already dominates.

    Q: Are there situations where algorithms are designed to sacrifice time (or space) based on actual use cases?

    In practical applications, most situations choose to sacrifice space for time. For example, with database indexes, we typically choose to build B+ trees or hash indexes, occupying substantial memory space in exchange for efficient queries of \\(O(\\log n)\\) or even \\(O(1)\\).

    In scenarios where space resources are precious, time may be sacrificed for space. For example, in embedded development, device memory is precious, and engineers may forgo using hash tables and choose to use array sequential search to save memory usage, at the cost of slower searches.

    ","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   Time Complexity","text":"

    Runtime can intuitively and accurately reflect the efficiency of an algorithm. If we want to accurately estimate the runtime of a piece of code, how should we proceed?

    1. Determine the running platform, including hardware configuration, programming language, system environment, etc., as these factors all affect code execution efficiency.
    2. Evaluate the runtime required for various computational operations, for example, an addition operation + requires 1 ns, a multiplication operation * requires 10 ns, a print operation print() requires 5 ns, etc.
    3. Count all computational operations in the code, and sum the execution times of all operations to obtain the runtime.

    For example, in the following code, the input data size is \\(n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # On a certain running platform\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # Loop n times\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
    // On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
    // On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
    // On a certain running platform\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
    // On a certain running platform\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // Loop n times\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
    // On a certain running platform\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // Loop n times\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
    // On a certain running platform\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // Loop n times\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // On a certain running platform\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // Loop n times\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // On a certain running platform\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // Loop n times\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
    // On a certain running platform\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // Loop n times\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
    // On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
    // On a certain running platform\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // Loop n times\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
    # On a certain running platform\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # Loop n times\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

    According to the above method, the algorithm's runtime can be obtained as \\((6n + 12)\\) ns:

    \\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\]

    In reality, however, trying to count an algorithm's exact runtime is neither practical nor realistic. First, we do not want to tie the estimated time to the running platform, because algorithms need to run on many different platforms. Second, it is difficult to know the runtime of each type of operation, which makes the estimation process extremely difficult.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231-counting-time-growth-trends","level":2,"title":"2.3.1   Counting Time Growth Trends","text":"

    Time complexity analysis does not count the algorithm's runtime, but rather counts the growth trend of the algorithm's runtime as the data volume increases.

    The concept of \"time growth trend\" is rather abstract; let us understand it through an example. Suppose the input data size is \\(n\\), and given three algorithms A, B, and C:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # Time complexity of algorithm A: constant order\ndef algorithm_A(n: int):\n    print(0)\n# Time complexity of algorithm B: linear order\ndef algorithm_B(n: int):\n    for _ in range(n):\n        print(0)\n# Time complexity of algorithm C: constant order\ndef algorithm_C(n: int):\n    for _ in range(1000000):\n        print(0)\n
    // Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    cout << 0 << endl;\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        cout << 0 << endl;\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        cout << 0 << endl;\n    }\n}\n
    // Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    System.out.println(0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        System.out.println(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        System.out.println(0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nvoid AlgorithmA(int n) {\n    Console.WriteLine(0);\n}\n// Time complexity of algorithm B: linear order\nvoid AlgorithmB(int n) {\n    for (int i = 0; i < n; i++) {\n        Console.WriteLine(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid AlgorithmC(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        Console.WriteLine(0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfunc algorithm_A(n int) {\n    fmt.Println(0)\n}\n// Time complexity of algorithm B: linear order\nfunc algorithm_B(n int) {\n    for i := 0; i < n; i++ {\n        fmt.Println(0)\n    }\n}\n// Time complexity of algorithm C: constant order\nfunc algorithm_C(n int) {\n    for i := 0; i < 1000000; i++ {\n        fmt.Println(0)\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfunc algorithmA(n: Int) {\n    print(0)\n}\n\n// Time complexity of algorithm B: linear order\nfunc algorithmB(n: Int) {\n    for _ in 0 ..< n {\n        print(0)\n    }\n}\n\n// Time complexity of algorithm C: constant order\nfunc algorithmC(n: Int) {\n    for _ in 0 ..< 1_000_000 {\n        print(0)\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfunction algorithm_A(n) {\n    console.log(0);\n}\n// Time complexity of algorithm B: linear order\nfunction algorithm_B(n) {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfunction algorithm_C(n) {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfunction algorithm_A(n: number): void {\n    console.log(0);\n}\n// Time complexity of algorithm B: linear order\nfunction algorithm_B(n: number): void {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfunction algorithm_C(n: number): void {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nvoid algorithmA(int n) {\n  print(0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithmB(int n) {\n  for (int i = 0; i < n; i++) {\n    print(0);\n  }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithmC(int n) {\n  for (int i = 0; i < 1000000; i++) {\n    print(0);\n  }\n}\n
    // Time complexity of algorithm A: constant order\nfn algorithm_A(n: i32) {\n    println!(\"{}\", 0);\n}\n// Time complexity of algorithm B: linear order\nfn algorithm_B(n: i32) {\n    for _ in 0..n {\n        println!(\"{}\", 0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfn algorithm_C(n: i32) {\n    for _ in 0..1000000 {\n        println!(\"{}\", 0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    printf(\"%d\", 0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        printf(\"%d\", 0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        printf(\"%d\", 0);\n    }\n}\n
    // Time complexity of algorithm A: constant order\nfun algoritm_A(n: Int) {\n    println(0)\n}\n// Time complexity of algorithm B: linear order\nfun algorithm_B(n: Int) {\n    for (i in 0..<n){\n        println(0)\n    }\n}\n// Time complexity of algorithm C: constant order\nfun algorithm_C(n: Int) {\n    for (i in 0..<1000000) {\n        println(0)\n    }\n}\n
    # Time complexity of algorithm A: constant order\ndef algorithm_A(n)\n    puts 0\nend\n\n# Time complexity of algorithm B: linear order\ndef algorithm_B(n)\n    (0...n).each { puts 0 }\nend\n\n# Time complexity of algorithm C: constant order\ndef algorithm_C(n)\n    (0...1_000_000).each { puts 0 }\nend\n

    Figure 2-7 shows the time complexity of the above three algorithm functions.

    • Algorithm A has only \\(1\\) print operation, and the algorithm's runtime does not grow as \\(n\\) increases. We call the time complexity of this algorithm \"constant order\".
    • In algorithm B, the print operation needs to loop \\(n\\) times, and the algorithm's runtime grows linearly as \\(n\\) increases. The time complexity of this algorithm is called \"linear order\".
    • In algorithm C, the print operation needs to loop \\(1000000\\) times. Although the runtime is very long, it is independent of the input data size \\(n\\). Therefore, the time complexity of C is the same as A, still \"constant order\".

    Figure 2-7   Time growth trends of algorithms A, B, and C

    Compared to directly counting the algorithm's runtime, what are the characteristics of time complexity analysis?

    • Time complexity can effectively evaluate algorithm efficiency. For example, the runtime of algorithm B grows linearly; when \\(n > 1\\) it is slower than algorithm A, and when \\(n > 1000000\\) it is slower than algorithm C. In fact, as long as the input data size \\(n\\) is sufficiently large, an algorithm with \"constant order\" complexity will always be superior to one with \"linear order\" complexity, which is precisely the meaning of time growth trend.
    • The derivation method for time complexity is simpler. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same \"unit time\", reducing \"tracking the runtime of each operation\" to \"counting the number of operations\", which greatly reduces the difficulty of estimation.
    • Time complexity also has certain limitations. For example, although algorithms A and C have the same time complexity, their actual runtimes differ significantly. Similarly, although algorithm B has a higher time complexity than C, when the input data size \\(n\\) is small, algorithm B is clearly superior to algorithm C. In such cases, it is often difficult to judge the efficiency of algorithms based solely on time complexity. Of course, despite the above issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency.
    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232-asymptotic-upper-bound-of-functions","level":2,"title":"2.3.2   Asymptotic Upper Bound of Functions","text":"

    Given a function with input size \\(n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    def algorithm(n: int):\n    a = 1      # +1\n    a = a + 1  # +1\n    a = a * 2  # +1\n    # Loop n times\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    // Loop n times\n    for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\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    // Loop n times\n    for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\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    // Loop n times\n    for (int i = 0; i < n; i++) {   // +1 (i++ is executed each round)\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    // Loop n times\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    // Loop n times\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    // Loop n times\n    for(let i = 0; i < n; i++){ // +1 (i++ is executed each round)\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    // Loop n times\n    for(let i = 0; i < n; i++){ // +1 (i++ is executed each round)\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  // Loop n times\n  for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\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    // Loop n times\n    for _ in 0..n { // +1 (i++ is executed each round)\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    // Loop n times\n    for (int i = 0; i < n; i++) {   // +1 (i++ is executed each round)\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    // Loop n times\n    for (i in 0..<n) { // +1 (i++ is executed each round)\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    # Loop n times\n    (0...n).each do # +1\n        puts 0      # +1\n    end\nend\n

    Let the number of operations of the algorithm be a function of the input data size \\(n\\), denoted as \\(T(n)\\). Then the number of operations of the above function is:

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

    \\(T(n)\\) is a linear function, indicating that its runtime growth trend is linear, and therefore its time complexity is linear order.

    We denote the time complexity of linear order as \\(O(n)\\). This mathematical symbol is called big-\\(O\\) notation, representing the asymptotic upper bound of the function \\(T(n)\\).

    Time complexity analysis essentially calculates the asymptotic upper bound of \"the number of operations \\(T(n)\\)\", which has a clear mathematical definition.

    Asymptotic upper bound of functions

    If there exist positive real numbers \\(c\\) and \\(n_0\\) such that for all \\(n > n_0\\), we have \\(T(n) \\leq c \\cdot f(n)\\), then \\(f(n)\\) can be considered as an asymptotic upper bound of \\(T(n)\\), denoted as \\(T(n) = O(f(n))\\).

    As shown in Figure 2-8, calculating the asymptotic upper bound is to find a function \\(f(n)\\) such that when \\(n\\) tends to infinity, \\(T(n)\\) and \\(f(n)\\) are at the same growth level, differing only by a constant coefficient \\(c\\).

    Figure 2-8   Asymptotic upper bound of a function

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233-derivation-method","level":2,"title":"2.3.3   Derivation Method","text":"

    The idea of an asymptotic upper bound is somewhat mathematical. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice.

    According to the definition, after determining \\(f(n)\\), we can obtain the time complexity \\(O(f(n))\\). So how do we determine the asymptotic upper bound \\(f(n)\\)? Overall, it is divided into two steps: first count the number of operations, then determine the asymptotic upper bound.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-step-1-count-the-number-of-operations","level":3,"title":"1.   Step 1: Count the Number of Operations","text":"

    For code, count from top to bottom line by line. However, since the constant coefficient \\(c\\) in \\(c \\cdot f(n)\\) above can be of any size, coefficients and constant terms in the number of operations \\(T(n)\\) can all be ignored. According to this principle, the following counting simplification techniques can be summarized.

    1. Ignore constants in \\(T(n)\\). Because they are all independent of \\(n\\), they do not affect time complexity.
    2. Omit all coefficients. For example, looping \\(2n\\) times, \\(5n + 1\\) times, etc., can all be simplified as \\(n\\) times, because the coefficient before \\(n\\) does not affect time complexity.
    3. Use multiplication for nested loops. The total number of operations equals the product of the number of operations in the outer and inner loops, with each layer of loop still able to apply techniques 1. and 2. separately.

    Given a function, we can use the above techniques to count the number of operations:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    def algorithm(n: int):\n    a = 1      # +0 (Technique 1)\n    a = a + n  # +0 (Technique 1)\n    # +n (Technique 2)\n    for i in range(5 * n + 1):\n        print(0)\n    # +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        cout << 0 << endl;\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        System.out.println(0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        Console.WriteLine(0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n  // +0 (Technique 1)\n    // +n (Technique 2)\n    for i := 0; i < 5 * n + 1; i++ {\n        fmt.Println(0)\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n // +0 (Technique 1)\n    // +n (Technique 2)\n    for _ in 0 ..< (5 * n + 1) {\n        print(0)\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n  a = a + n; // +0 (Technique 1)\n  // +n (Technique 2)\n  for (int i = 0; i < 5 * n + 1; i++) {\n    print(0);\n  }\n  // +n*n (Technique 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 (Technique 1)\n    a = a + n;        // +0 (Technique 1)\n\n    // +n (Technique 2)\n    for i in 0..(5 * n + 1) {\n        println!(\"{}\", 0);\n    }\n\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        printf(\"%d\", 0);\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n   // +0 (Technique 1)\n    // +n (Technique 2)\n    for (i in 0..<5 * n + 1) {\n        println(0)\n    }\n    // +n*n (Technique 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 (Technique 1)\n    a = a + n   # +0 (Technique 1)\n    # +n (Technique 2)\n    (0...(5 * n + 1)).each do { puts 0 }\n    # +n*n (Technique 3)\n    (0...(2 * n)).each do\n        (0...(n + 1)).each do { puts 0 }\n    end\nend\n

    The following formula shows the counting results before and after using the above techniques; both derive a time complexity of \\(O(n^2)\\).

    \\[ \\begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \\text{Complete count (-.-|||)} \\newline & = 2n^2 + 7n + 3 \\newline T(n) & = n^2 + n & \\text{Simplified count (o.O)} \\end{aligned} \\]","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-step-2-determine-the-asymptotic-upper-bound","level":3,"title":"2.   Step 2: Determine the Asymptotic Upper Bound","text":"

    Time complexity is determined by the highest-order term in \\(T(n)\\). This is because as \\(n\\) tends to infinity, the highest-order term will play a dominant role, and the influence of other terms can be ignored.

    Table 2-2 shows some examples, where some exaggerated values are used to emphasize the conclusion that \"coefficients cannot shake the order\". When \\(n\\) tends to infinity, these constants become insignificant.

    Table 2-2   Time complexities corresponding to different numbers of operations

    Number of Operations \\(T(n)\\) Time Complexity \\(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":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234-common-types","level":2,"title":"2.3.4   Common Types","text":"

    Let the input data size be \\(n\\). Common time complexity types are shown in Figure 2-9 (arranged in order from low to high).

    \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n \\log n) < O(n^2) < O(2^n) < O(n!) \\newline & \\text{Constant} < \\text{Logarithmic} < \\text{Linear} < \\text{Linearithmic} < \\text{Quadratic} < \\text{Exponential} < \\text{Factorial} \\end{aligned} \\]

    Figure 2-9   Common time complexity types

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-constant-order-o1","level":3,"title":"1.   Constant Order \\(O(1)\\)","text":"

    The number of operations in constant order is independent of the input data size \\(n\\), meaning it does not change as \\(n\\) changes.

    In the following function, although the value of size may be large, it is independent of the input data size \\(n\\), so the time complexity remains \\(O(1)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def constant(n: int) -> int:\n    \"\"\"Constant order\"\"\"\n    count = 0\n    size = 100000\n    for _ in range(size):\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    /* Constant order */\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
    ### Constant time ###\ndef constant(n)\n  count = 0\n  size = 100000\n\n  (0...size).each { count += 1 }\n\n  count\nend\n
    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-linear-order-on","level":3,"title":"2.   Linear Order \\(O(n)\\)","text":"

    The number of operations in linear order grows linearly relative to the input data size \\(n\\). Linear order typically appears in single-layer loops:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def linear(n: int) -> int:\n    \"\"\"Linear order\"\"\"\n    count = 0\n    for _ in range(n):\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\nfunction linear(n) {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
    time_complexity.ts
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    /* Linear order */\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
    ### Linear time ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n

    Operations such as traversing arrays and traversing linked lists have a time complexity of \\(O(n)\\), where \\(n\\) is the length of the array or linked list:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def array_traversal(nums: list[int]) -> int:\n    \"\"\"Linear order (traversing array)\"\"\"\n    count = 0\n    # Number of iterations is proportional to the array length\n    for num in nums:\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Linear order (traversing array) */\nint arrayTraversal(vector<int> &nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.java
    /* Linear order (traversing array) */\nint arrayTraversal(int[] nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.cs
    /* Linear order (traversing array) */\nint ArrayTraversal(int[] nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    foreach (int num in nums) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.go
    /* Linear order (traversing array) */\nfunc arrayTraversal(nums []int) int {\n    count := 0\n    // Number of iterations is proportional to the array length\n    for range nums {\n        count++\n    }\n    return count\n}\n
    time_complexity.swift
    /* Linear order (traversing array) */\nfunc arrayTraversal(nums: [Int]) -> Int {\n    var count = 0\n    // Number of iterations is proportional to the array length\n    for _ in nums {\n        count += 1\n    }\n    return count\n}\n
    time_complexity.js
    /* Linear order (traversing array) */\nfunction arrayTraversal(nums) {\n    let count = 0;\n    // Number of iterations is proportional to the array length\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.ts
    /* Linear order (traversing array) */\nfunction arrayTraversal(nums: number[]): number {\n    let count = 0;\n    // Number of iterations is proportional to the array length\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.dart
    /* Linear order (traversing array) */\nint arrayTraversal(List<int> nums) {\n  int count = 0;\n  // Number of iterations is proportional to the array length\n  for (var _num in nums) {\n    count++;\n  }\n  return count;\n}\n
    time_complexity.rs
    /* Linear order (traversing array) */\nfn array_traversal(nums: &[i32]) -> i32 {\n    let mut count = 0;\n    // Number of iterations is proportional to the array length\n    for _ in nums {\n        count += 1;\n    }\n    count\n}\n
    time_complexity.c
    /* Linear order (traversing array) */\nint arrayTraversal(int *nums, int n) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
    time_complexity.kt
    /* Linear order (traversing array) */\nfun arrayTraversal(nums: IntArray): Int {\n    var count = 0\n    // Number of iterations is proportional to the array length\n    for (num in nums) {\n        count++\n    }\n    return count\n}\n
    time_complexity.rb
    ### Linear time (array traversal) ###\ndef array_traversal(nums)\n  count = 0\n\n  # Number of iterations is proportional to the array length\n  for num in nums\n    count += 1\n  end\n\n  count\nend\n

    It is worth noting that the input data size \\(n\\) should be determined according to the type of input data. For example, in the first example, the variable \\(n\\) is the input data size; in the second example, the array length \\(n\\) is the data size.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-quadratic-order-on2","level":3,"title":"3.   Quadratic Order \\(O(n^2)\\)","text":"

    The number of operations in quadratic order grows quadratically relative to the input data size \\(n\\). Quadratic order typically appears in nested loops, where both the outer and inner loops have a time complexity of \\(O(n)\\), resulting in an overall time complexity of \\(O(n^2)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def quadratic(n: int) -> int:\n    \"\"\"Quadratic order\"\"\"\n    count = 0\n    # Number of iterations is quadratically related to the data size n\n    for i in range(n):\n        for j in range(n):\n            count += 1\n    return count\n
    time_complexity.cpp
    /* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nint Quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfunc quadratic(n int) int {\n    count := 0\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfunc quadratic(n: Int) -> Int {\n    var count = 0\n    // Number of iterations is quadratically related to the data size n\n    for _ in 0 ..< n {\n        for _ in 0 ..< n {\n            count += 1\n        }\n    }\n    return count\n}\n
    time_complexity.js
    /* Exponential order */\nfunction quadratic(n) {\n    let count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfunction quadratic(n: number): number {\n    let count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nint quadratic(int n) {\n  int count = 0;\n  // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfn quadratic(n: i32) -> i32 {\n    let mut count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for _ in 0..n {\n        for _ in 0..n {\n            count += 1;\n        }\n    }\n    count\n}\n
    time_complexity.c
    /* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size 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
    /* Exponential order */\nfun quadratic(n: Int): Int {\n    var count = 0\n    // Number of iterations is quadratically related to the data size 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
    ### Quadratic time ###\ndef quadratic(n)\n  count = 0\n\n  # Number of iterations is quadratically related to the data size 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

    Figure 2-10 compares constant order, linear order, and quadratic order time complexities.

    Figure 2-10   Time complexities of constant, linear, and quadratic orders

    Taking bubble sort as an example, the outer loop executes \\(n - 1\\) times, and the inner loop executes \\(n-1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\) times, averaging \\(n / 2\\) times, resulting in a time complexity of \\(O((n - 1) n / 2) = O(n^2)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def bubble_sort(nums: list[int]) -> int:\n    \"\"\"Quadratic order (bubble sort)\"\"\"\n    count = 0  # Counter\n    # Outer loop: unsorted range is [0, i]\n    for i in range(len(nums) - 1, 0, -1):\n        # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                tmp: int = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3  # Element swap includes 3 unit operations\n    return count\n
    time_complexity.cpp
    /* Quadratic order (bubble sort) */\nint bubbleSort(vector<int> &nums) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.java
    /* Quadratic order (bubble sort) */\nint bubbleSort(int[] nums) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.cs
    /* Quadratic order (bubble sort) */\nint BubbleSort(int[] nums) {\n    int count = 0;  // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                count += 3;  // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.go
    /* Quadratic order (bubble sort) */\nfunc bubbleSort(nums []int) int {\n    count := 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                tmp := nums[j]\n                nums[j] = nums[j+1]\n                nums[j+1] = tmp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
    time_complexity.swift
    /* Quadratic order (bubble sort) */\nfunc bubbleSort(nums: inout [Int]) -> Int {\n    var count = 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
    time_complexity.js
    /* Quadratic order (bubble sort) */\nfunction bubbleSort(nums) {\n    let count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.ts
    /* Quadratic order (bubble sort) */\nfunction bubbleSort(nums: number[]): number {\n    let count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.dart
    /* Quadratic order (bubble sort) */\nint bubbleSort(List<int> nums) {\n  int count = 0; // Counter\n  // Outer loop: unsorted range is [0, i]\n  for (var i = nums.length - 1; i > 0; i--) {\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (var j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        count += 3; // Element swap includes 3 unit operations\n      }\n    }\n  }\n  return count;\n}\n
    time_complexity.rs
    /* Quadratic order (bubble sort) */\nfn bubble_sort(nums: &mut [i32]) -> i32 {\n    let mut count = 0; // Counter\n\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    count\n}\n
    time_complexity.c
    /* Quadratic order (bubble sort) */\nint bubbleSort(int *nums, int n) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = n - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
    time_complexity.kt
    /* Quadratic order (bubble sort) */\nfun bubbleSort(nums: IntArray): Int {\n    var count = 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
    time_complexity.rb
    ### Quadratic time (bubble sort) ###\ndef bubble_sort(nums)\n  count = 0  # Counter\n\n  # Outer loop: unsorted range is [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Element swap includes 3 unit operations\n      end\n    end\n  end\n\n  count\nend\n
    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-exponential-order-o2n","level":3,"title":"4.   Exponential Order \\(O(2^n)\\)","text":"

    Biological \"cell division\" is a typical example of exponential order growth: the initial state is \\(1\\) cell, after one round of division it becomes \\(2\\), after two rounds it becomes \\(4\\), and so on; after \\(n\\) rounds of division there are \\(2^n\\) cells.

    Figure 2-11 and the following code simulate the cell division process, with a time complexity of \\(O(2^n)\\). Note that the input \\(n\\) represents the number of division rounds, and the return value count represents the total number of divisions.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def exponential(n: int) -> int:\n    \"\"\"Exponential order (loop implementation)\"\"\"\n    count = 0\n    base = 1\n    # Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint Exponential(int n) {\n    int count = 0, bas = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfunc exponential(n int) int {\n    count, base := 0, 1\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfunc exponential(n: Int) -> Int {\n    var count = 0\n    var base = 1\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfunction exponential(n) {\n    let count = 0,\n        base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfunction exponential(n: number): number {\n    let count = 0,\n        base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint exponential(int n) {\n  int count = 0, base = 1;\n  // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfn exponential(n: i32) -> i32 {\n    let mut count = 0;\n    let mut base = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0;\n    int bas = 1;\n    // Cells divide into two every round, forming sequence 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
    /* Exponential order (loop implementation) */\nfun exponential(n: Int): Int {\n    var count = 0\n    var base = 1\n    // Cells divide into two every round, forming sequence 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
    ### Exponential time (iterative) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # Cells divide into two every round, forming sequence 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

    Figure 2-11   Time complexity of exponential order

    In actual algorithms, exponential order often appears in recursive functions. For example, in the following code, it recursively splits in two, stopping after \\(n\\) splits:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def exp_recur(n: int) -> int:\n    \"\"\"Exponential order (recursive implementation)\"\"\"\n    if n == 1:\n        return 1\n    return exp_recur(n - 1) + exp_recur(n - 1) + 1\n
    time_complexity.cpp
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\nint ExpRecur(int n) {\n    if (n == 1) return 1;\n    return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n
    time_complexity.go
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\nfunction expRecur(n) {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
    time_complexity.ts
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\nint expRecur(int n) {\n  if (n == 1) return 1;\n  return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
    time_complexity.rs
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\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
    /* Exponential order (recursive implementation) */\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
    ### Exponential time (recursive) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n

    Exponential order growth is very rapid and is common in exhaustive methods (brute force search, backtracking, etc.). For problems with large data scales, exponential order is unacceptable and typically requires dynamic programming or greedy algorithms to solve.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-logarithmic-order-olog-n","level":3,"title":"5.   Logarithmic Order \\(O(\\log n)\\)","text":"

    In contrast to exponential order, logarithmic order reflects the situation of \"reducing to half each round\". Let the input data size be \\(n\\). Since it is reduced to half each round, the number of loops is \\(\\log_2 n\\), which is the inverse function of \\(2^n\\).

    Figure 2-12 and the following code simulate the process of \"reducing to half each round\", with a time complexity of \\(O(\\log_2 n)\\), abbreviated as \\(O(\\log n)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def logarithmic(n: int) -> int:\n    \"\"\"Logarithmic order (loop implementation)\"\"\"\n    count = 0\n    while n > 1:\n        n = n / 2\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    /* Logarithmic order (loop implementation) */\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
    ### Logarithmic time (iterative) ###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n

    Figure 2-12   Time complexity of logarithmic order

    Like exponential order, logarithmic order also commonly appears in recursive functions. The following code forms a recursion tree of height \\(\\log_2 n\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def log_recur(n: int) -> int:\n    \"\"\"Logarithmic order (recursive implementation)\"\"\"\n    if n <= 1:\n        return 0\n    return log_recur(n / 2) + 1\n
    time_complexity.cpp
    /* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.java
    /* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.cs
    /* Logarithmic order (recursive implementation) */\nint LogRecur(int n) {\n    if (n <= 1) return 0;\n    return LogRecur(n / 2) + 1;\n}\n
    time_complexity.go
    /* Logarithmic order (recursive implementation) */\nfunc logRecur(n int) int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n/2) + 1\n}\n
    time_complexity.swift
    /* Logarithmic order (recursive implementation) */\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
    /* Logarithmic order (recursive implementation) */\nfunction logRecur(n) {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.ts
    /* Logarithmic order (recursive implementation) */\nfunction logRecur(n: number): number {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.dart
    /* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n  if (n <= 1) return 0;\n  return logRecur(n ~/ 2) + 1;\n}\n
    time_complexity.rs
    /* Logarithmic order (recursive implementation) */\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
    /* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
    time_complexity.kt
    /* Logarithmic order (recursive implementation) */\nfun logRecur(n: Int): Int {\n    if (n <= 1)\n        return 0\n    return logRecur(n / 2) + 1\n}\n
    time_complexity.rb
    ### Logarithmic time (recursive) ###\ndef log_recur(n)\n  return 0 unless n > 1\n  log_recur(n / 2) + 1\nend\n

    Logarithmic order commonly appears in algorithms based on the divide-and-conquer strategy, reflecting the idea of repeatedly splitting a problem and simplifying it. It grows slowly and is the ideal time complexity second only to constant order.

    What is the base of \\(O(\\log n)\\)?

    To be precise, \"dividing into \\(m\\)\" corresponds to a time complexity of \\(O(\\log_m n)\\). And through the logarithmic base change formula, we can obtain time complexities with different bases that are equal:

    \\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\]

    That is to say, the base \\(m\\) can be converted without affecting the complexity. Therefore, we usually omit the base \\(m\\) and denote logarithmic order simply as \\(O(\\log n)\\).

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-linearithmic-order-on-log-n","level":3,"title":"6.   Linearithmic Order \\(O(n \\log n)\\)","text":"

    Linearithmic order commonly appears in nested loops, where the time complexities of the two layers of loops are \\(O(\\log n)\\) and \\(O(n)\\) respectively. The relevant code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def linear_log_recur(n: int) -> int:\n    \"\"\"Linearithmic order\"\"\"\n    if n <= 1:\n        return 1\n    # Divide into two, the scale of subproblems is reduced by half\n    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n    # Current subproblem contains n operations\n    for _ in range(n):\n        count += 1\n    return count\n
    time_complexity.cpp
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    /* Linearithmic order */\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
    ### Linearithmic time ###\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

    Figure 2-13 shows how linearithmic order is generated. Each level of the binary tree has a total of \\(n\\) operations, and the tree has \\(\\log_2 n + 1\\) levels, resulting in a time complexity of \\(O(n \\log n)\\).

    Figure 2-13   Time complexity of linearithmic order

    Mainstream sorting algorithms typically have a time complexity of \\(O(n \\log n)\\), such as quicksort, merge sort, and heap sort.

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-factorial-order-on","level":3,"title":"7.   Factorial Order \\(O(n!)\\)","text":"

    Factorial order corresponds to the mathematical \"permutation\" problem. Given \\(n\\) distinct elements, find all possible permutation schemes; the number of schemes is:

    \\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\]

    Factorials are typically implemented using recursion. As shown in Figure 2-14 and the following code, the first level splits into \\(n\\) branches, the second level splits into \\(n - 1\\) branches, and so on, until the \\(n\\)-th level when splitting stops:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def factorial_recur(n: int) -> int:\n    \"\"\"Factorial order (recursive implementation)\"\"\"\n    if n == 0:\n        return 1\n    count = 0\n    # Split from 1 into n\n    for _ in range(n):\n        count += factorial_recur(n - 1)\n    return count\n
    time_complexity.cpp
    /* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.java
    /* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.cs
    /* Factorial order (recursive implementation) */\nint FactorialRecur(int n) {\n    if (n == 0) return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += FactorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.go
    /* Factorial order (recursive implementation) */\nfunc factorialRecur(n int) int {\n    if n == 0 {\n        return 1\n    }\n    count := 0\n    // Split from 1 into n\n    for i := 0; i < n; i++ {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
    time_complexity.swift
    /* Factorial order (recursive implementation) */\nfunc factorialRecur(n: Int) -> Int {\n    if n == 0 {\n        return 1\n    }\n    var count = 0\n    // Split from 1 into n\n    for _ in 0 ..< n {\n        count += factorialRecur(n: n - 1)\n    }\n    return count\n}\n
    time_complexity.js
    /* Factorial order (recursive implementation) */\nfunction factorialRecur(n) {\n    if (n === 0) return 1;\n    let count = 0;\n    // Split from 1 into n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.ts
    /* Factorial order (recursive implementation) */\nfunction factorialRecur(n: number): number {\n    if (n === 0) return 1;\n    let count = 0;\n    // Split from 1 into n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
    time_complexity.dart
    /* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n  if (n == 0) return 1;\n  int count = 0;\n  // Split from 1 into n\n  for (var i = 0; i < n; i++) {\n    count += factorialRecur(n - 1);\n  }\n  return count;\n}\n
    time_complexity.rs
    /* Factorial order (recursive implementation) */\nfn factorial_recur(n: i32) -> i32 {\n    if n == 0 {\n        return 1;\n    }\n    let mut count = 0;\n    // Split from 1 into n\n    for _ in 0..n {\n        count += factorial_recur(n - 1);\n    }\n    count\n}\n
    time_complexity.c
    /* Factorial order (recursive implementation) */\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
    /* Factorial order (recursive implementation) */\nfun factorialRecur(n: Int): Int {\n    if (n == 0)\n        return 1\n    var count = 0\n    // Split from 1 into n\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
    time_complexity.rb
    ### Factorial time (recursive) ###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # Split from 1 into n\n  (0...n).each { count += factorial_recur(n - 1) }\n\n  count\nend\n

    Figure 2-14   Time complexity of factorial order

    Note that because when \\(n \\geq 4\\) we always have \\(n! > 2^n\\), factorial order grows faster than exponential order, and is also unacceptable for large \\(n\\).

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235-worst-best-and-average-time-complexities","level":2,"title":"2.3.5   Worst, Best, and Average Time Complexities","text":"

    The time efficiency of an algorithm is often not fixed, but is related to the distribution of the input data. Suppose we input an array nums of length \\(n\\), where nums consists of numbers from \\(1\\) to \\(n\\), with each number appearing only once, but the element order is randomly shuffled. The task is to return the index of element \\(1\\). We can draw the following conclusions.

    • When nums = [?, ?, ..., 1], i.e., when the last element is \\(1\\), it requires a complete traversal of the array, reaching worst-case time complexity \\(O(n)\\).
    • When nums = [1, ?, ?, ...], i.e., when the first element is \\(1\\), no matter how long the array is, there is no need to continue traversing, reaching best-case time complexity \\(\\Omega(1)\\).

    The \"worst-case time complexity\" corresponds to the function's asymptotic upper bound, denoted using big-\\(O\\) notation. Correspondingly, the \"best-case time complexity\" corresponds to the function's asymptotic lower bound, denoted using \\(\\Omega\\) notation:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py
    def random_numbers(n: int) -> list[int]:\n    \"\"\"Generate an array with elements: 1, 2, ..., n, shuffled in order\"\"\"\n    # Generate array nums =: 1, 2, 3, ..., n\n    nums = [i for i in range(1, n + 1)]\n    # Randomly shuffle array elements\n    random.shuffle(nums)\n    return nums\n\ndef find_one(nums: list[int]) -> int:\n    \"\"\"Find the index of number 1 in array nums\"\"\"\n    for i in range(len(nums)):\n        # When element 1 is at the head of the array, best time complexity O(1) is achieved\n        # When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1:\n            return i\n    return -1\n
    worst_best_time_complexity.cpp
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nvector<int> randomNumbers(int n) {\n    vector<int> nums(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Use system time to generate random seed\n    unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n    // Randomly shuffle array elements\n    shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(vector<int> &nums) {\n    for (int i = 0; i < nums.size(); i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
    worst_best_time_complexity.java
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint[] randomNumbers(int n) {\n    Integer[] nums = new Integer[n];\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nint findOne(int[] nums) {\n    for (int i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
    worst_best_time_complexity.cs
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint[] RandomNumbers(int n) {\n    int[] nums = new int[n];\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nint FindOne(int[] nums) {\n    for (int i = 0; i < nums.Length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
    worst_best_time_complexity.go
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunc randomNumbers(n int) []int {\n    nums := make([]int, n)\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for i := 0; i < n; i++ {\n        nums[i] = i + 1\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nfunc findOne(nums []int) int {\n    for i := 0; i < len(nums); i++ {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
    worst_best_time_complexity.swift
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunc randomNumbers(n: Int) -> [Int] {\n    // Generate array nums = { 1, 2, 3, ..., n }\n    var nums = Array(1 ... n)\n    // Randomly shuffle array elements\n    nums.shuffle()\n    return nums\n}\n\n/* Find the index of number 1 in array nums */\nfunc findOne(nums: [Int]) -> Int {\n    for i in nums.indices {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
    worst_best_time_complexity.js
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunction randomNumbers(n) {\n    const nums = Array(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nfunction findOne(nums) {\n    for (let i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
    worst_best_time_complexity.ts
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunction randomNumbers(n: number): number[] {\n    const nums = Array(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nfunction findOne(nums: number[]): number {\n    for (let i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
    worst_best_time_complexity.dart
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nList<int> randomNumbers(int n) {\n  final nums = List.filled(n, 0);\n  // Generate array nums = { 1, 2, 3, ..., n }\n  for (var i = 0; i < n; i++) {\n    nums[i] = i + 1;\n  }\n  // Randomly shuffle array elements\n  nums.shuffle();\n\n  return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(List<int> nums) {\n  for (var i = 0; i < nums.length; i++) {\n    // When element 1 is at the head of the array, best time complexity O(1) is achieved\n    // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n    if (nums[i] == 1) return i;\n  }\n\n  return -1;\n}\n
    worst_best_time_complexity.rs
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfn random_numbers(n: i32) -> Vec<i32> {\n    // Generate array nums = { 1, 2, 3, ..., n }\n    let mut nums = (1..=n).collect::<Vec<i32>>();\n    // Randomly shuffle array elements\n    nums.shuffle(&mut thread_rng());\n    nums\n}\n\n/* Find the index of number 1 in array nums */\nfn find_one(nums: &[i32]) -> Option<usize> {\n    for i in 0..nums.len() {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return Some(i);\n        }\n    }\n    None\n}\n
    worst_best_time_complexity.c
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint *randomNumbers(int n) {\n    // Allocate heap memory (create 1D variable-length array: n elements of type int)\n    int *nums = (int *)malloc(n * sizeof(int));\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nint findOne(int *nums, int n) {\n    for (int i = 0; i < n; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
    worst_best_time_complexity.kt
    /* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfun randomNumbers(n: Int): Array<Int?> {\n    val nums = IntArray(n)\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (i in 0..<n) {\n        nums[i] = i + 1\n    }\n    // Randomly shuffle array elements\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/* Find the index of number 1 in array nums */\nfun findOne(nums: Array<Int?>): Int {\n    for (i in nums.indices) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i\n    }\n    return -1\n}\n
    worst_best_time_complexity.rb
    ### Generate array with elements: 1, 2, ..., n, shuffled ###\ndef random_numbers(n)\n  # Generate array nums =: 1, 2, 3, ..., n\n  nums = Array.new(n) { |i| i + 1 }\n  # Randomly shuffle array elements\n  nums.shuffle!\nend\n\n### Find index of number 1 in array nums ###\ndef find_one(nums)\n  for i in 0...nums.length\n    # When element 1 is at the head of the array, best time complexity O(1) is achieved\n    # When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n    return i if nums[i] == 1\n  end\n\n  -1\nend\n

    It is worth noting that we rarely use best-case time complexity in practice, because it can usually only be achieved with a very small probability and may be somewhat misleading. The worst-case time complexity is more practical because it gives a safety value for efficiency, allowing us to use the algorithm with confidence.

    From the above example, we can see that both worst-case and best-case time complexities arise only under particular input distributions, which may occur with very low probability and may not truly reflect the algorithm's running efficiency. In contrast, average time complexity can reflect the algorithm's running efficiency under random input data, denoted using the \\(\\Theta\\) notation.

    For some algorithms, we can simply derive the average case under random data distribution. For example, in the above example, since the input array is shuffled, the probability of element \\(1\\) appearing at any index is equal, so the algorithm's average number of loops is half the array length \\(n / 2\\), giving an average time complexity of \\(\\Theta(n / 2) = \\Theta(n)\\).

    But for more complex algorithms, calculating average time complexity is often quite difficult, because it is hard to analyze the overall mathematical expectation under data distribution. In this case, we usually use worst-case time complexity as the criterion for judging algorithm efficiency.

    Why is the \\(\\Theta\\) symbol rarely seen?

    This may be because the \\(O\\) symbol is too catchy, so we often use it to represent average time complexity. But strictly speaking, this practice is not standard. In this book and other materials, if you encounter expressions like \"average time complexity \\(O(n)\\)\", please understand it directly as \\(\\Theta(n)\\).

    ","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"Chapter 3.   Data Structures","text":"

    Abstract

    Data structures are like a sturdy and diverse framework.

    It provides a blueprint for the orderly organization of data, upon which algorithms come to life.

    ","path":["Chapter 3. Data Structures","Chapter 3.   Data Structures"],"tags":[]},{"location":"chapter_data_structure/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 3.1   Classification of Data Structures
    • 3.2   Basic Data Types
    • 3.3   Number Encoding *
    • 3.4   Character Encoding *
    • 3.5   Summary
    ","path":["Chapter 3. Data Structures","Chapter 3.   Data Structures"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   Basic Data Types","text":"

    When we talk about data stored in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these kinds of data are organized in different ways, they are all composed of various basic data types.

    Basic data types are types that the CPU can directly operate on, and they are directly used in algorithms, mainly including the following.

    • Integer types byte, short, int, long.
    • Floating-point types float, double, used to represent decimal numbers.
    • Character type char, used to represent letters, punctuation marks, and even emojis in various languages.
    • Boolean type bool, used to represent \"yes\" and \"no\" judgments.

    Basic data types are stored in binary form in computers. A binary digit is one bit. In most modern operating systems, \\(1\\) byte consists of \\(8\\) bits.

    The range of values for basic data types depends on the size of the space they occupy. Below is an example using Java.

    • Integer type byte occupies \\(1\\) byte = \\(8\\) bits, and can represent \\(2^{8}\\) numbers.
    • Integer type int occupies \\(4\\) bytes = \\(32\\) bits, and can represent \\(2^{32}\\) numbers.

    The following table lists the space occupied, value ranges, and default values of various basic data types in Java. You don't need to memorize this table; a general understanding is sufficient, and you can refer to it when needed.

    Table 3-1   Space occupied and value ranges of basic data types

    Type Symbol Space Occupied Minimum Value Maximum Value Default Value Integer byte 1 byte \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) short 2 bytes \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) int 4 bytes \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) long 8 bytes \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) Float float 4 bytes \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) double 8 bytes \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) Character char 2 bytes \\(0\\) \\(2^{16} - 1\\) \\(0\\) Boolean bool 1 byte \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\)

    Please note that Table 3-1 applies specifically to Java's basic data types. Each programming language has its own type definitions, and their space usage, value ranges, and default values may vary.

    • In Python, the integer type int can be of any size, limited only by available memory; the floating-point type float is double-precision 64-bit; there is no char type, a single character is actually a string str of length 1.
    • C and C++ do not explicitly specify the size of basic data types, which varies by implementation and platform. The above table follows the LP64 data model, which is used in Unix 64-bit operating systems including Linux and macOS.
    • The size of character char is 1 byte in C and C++, and in most programming languages it depends on the specific character encoding method, as detailed in the \"Character Encoding\" section.
    • Even though representing a boolean value requires only 1 bit (\\(0\\) or \\(1\\)), it is usually stored as 1 byte in memory. This is because modern computer CPUs typically use 1 byte as the minimum addressable memory unit.

    So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. Here, the emphasis is on the \"structure\", not the \"data\".

    If we want to represent \"a row of numbers\", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but whether the stored content is integer int, floating-point float, or character char is unrelated to the \"data structure\".

    In other words, basic data types provide the \"content type\" of data, while data structures provide the \"organization method\" of data. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including int, float, char, bool, etc.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # Initialize arrays using various basic data types\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# In Python, characters are actually strings of length 1\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Python lists can freely store various basic data types and object references\ndata = [0, 0.0, 'a', False, ListNode(0)]\n
    // Initialize arrays using various basic data types\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n
    // Initialize arrays using various basic data types\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n
    // Initialize arrays using various basic data types\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n
    // Initialize arrays using various basic data types\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n
    // Initialize arrays using various basic data types\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 arrays can freely store various basic data types and objects\nconst array = [0, 0.0, 'a', false];\n
    // Initialize arrays using various basic data types\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n
    // Initialize arrays using various basic data types\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
    // Initialize arrays using various basic data types\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
    // Initialize arrays using various basic data types\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n
    // Initialize arrays using various basic data types\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n
    # Ruby lists can freely store various basic data types and object references\ndata = [0, 0.0, 'a', false, ListNode(0)]\n
    Visualized Execution

    https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

    ","path":["Chapter 3. Data Structures","3.2   Basic Data Types"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   Character Encoding *","text":"

    In computers, all data is stored in binary form, and character char is no exception. To represent characters, we need to establish a \"character set\" that defines a one-to-one correspondence between each character and binary numbers. With a character set, computers can convert binary numbers to characters by looking up the table.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii-character-set","level":2,"title":"3.4.1   ASCII Character Set","text":"

    ASCII code is the earliest character set, with the full name American Standard Code for Information Interchange. It uses 7 binary bits (the lower 7 bits of one byte) to represent a character, and can represent a maximum of 128 different characters. As shown in Figure 3-6, ASCII code includes uppercase and lowercase English letters, numbers 0 ~ 9, some punctuation marks, and some control characters (such as newline and tab).

    Figure 3-6   ASCII code

    However, ASCII code can only represent English. With the globalization of computers, a character set called EASCII that can represent more languages emerged. It expands from the 7-bit basis of ASCII to 8 bits, and can represent 256 different characters.

    Worldwide, a batch of EASCII character sets suitable for different regions have appeared successively. The first 128 characters of these character sets are unified as ASCII code, and the last 128 characters are defined differently to adapt to the needs of different languages.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk-character-set","level":2,"title":"3.4.2   GBK Character Set","text":"

    Later, people found that EASCII still could not provide enough characters for many languages. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used in everyday life. In 1980, the China National Standardization Administration released the GB2312 character set, which included 6,763 Chinese characters, basically meeting the needs of computer processing for Chinese.

    However, GB2312 cannot handle some rare characters and traditional Chinese characters. The GBK character set is an extension based on GB2312, which includes a total of 21,886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented using one byte, and Chinese characters are represented using two bytes.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode-character-set","level":2,"title":"3.4.3   Unicode Character Set","text":"

    With the vigorous development of computer technology, character sets and encoding standards flourished, which brought many problems. On the one hand, these character sets generally only define characters for specific languages and cannot work normally in multilingual environments. On the other hand, multiple character set standards exist for the same language, and if two computers use different encoding standards, garbled characters will appear during information transmission.

    Researchers of that era thought: If a sufficiently complete character set were released to include all languages and symbols in the world, wouldn't that solve problems in cross-language environments and eliminate garbled text? Driven by this idea, a large and comprehensive character set, Unicode, was born.

    Unicode, or Unified Code, can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards. Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages.

    As a universal character set, Unicode essentially assigns each character a unique \"code point\" (character identifier), whose range is U+0000 to U+10FFFF, forming a unified character numbering space. However, Unicode does not specify how to store these character code points in computers. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters?

    For the above problem, a straightforward solution is to store all characters as equal-length encodings. As shown in Figure 3-7, each character in \"Hello\" occupies 1 byte, and each character in \"算法\" (algorithm) occupies 2 bytes. We can encode all characters in \"Hello 算法\" as 2 bytes in length by padding the high bits with 0. In this way, the system can parse one character every 2 bytes and restore the content of this phrase.

    Figure 3-7   Unicode encoding example

    However, ASCII code has already proven to us that encoding English only requires 1 byte. If the above scheme is adopted, the size of English text will be twice that under ASCII encoding, which is very wasteful of memory space. Therefore, we need a more efficient Unicode encoding method.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8-encoding","level":2,"title":"3.4.4   UTF-8 Encoding","text":"

    Currently, UTF-8 has become the most widely used Unicode encoding method internationally. It is a variable-length encoding that uses 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters only require 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters require 3 bytes, and some other rare characters require 4 bytes.

    The encoding rules of UTF-8 are not complicated and can be divided into the following two cases.

    • For 1-byte characters, set the highest bit to \\(0\\), and set the remaining 7 bits to the Unicode code point. It is worth noting that ASCII characters occupy the first 128 code points in the Unicode character set. That is to say, UTF-8 encoding is backward compatible with ASCII code. This means we can use UTF-8 to parse very old ASCII code text.
    • For characters with a length of \\(n\\) bytes (where \\(n > 1\\)), set the highest \\(n\\) bits of the first byte to \\(1\\), and set the \\((n + 1)\\)-th bit to \\(0\\); starting from the second byte, set the highest 2 bits of each byte to \\(10\\); use all remaining bits to fill in the Unicode code point of the character.

    Figure 3-8 shows the UTF-8 encoding corresponding to \"Hello 算法\". It can be observed that since the highest \\(n\\) bits are all set to \\(1\\), the system can determine that the character length is \\(n\\) by counting the leading \\(1\\) bits.

    But why set the highest 2 bits of all other bytes to \\(10\\)? In fact, this \\(10\\) can serve as a check symbol. Assuming the system starts parsing text from an incorrect byte, the \\(10\\) at the beginning of the byte can help the system quickly determine an anomaly.

    The reason for using \\(10\\) as a check symbol is that under UTF-8 encoding rules, it is impossible for a character's highest two bits to be \\(10\\). This conclusion can be proven by contradiction: assuming the highest two bits of a character are \\(10\\), it means the length of the character is \\(1\\), corresponding to ASCII code. However, the highest bit of ASCII code should be \\(0\\), which contradicts the assumption.

    Figure 3-8   UTF-8 encoding example

    In addition to UTF-8, common encoding methods also include the following two.

    • UTF-16 encoding: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters need to use 4 bytes. For 2-byte characters, UTF-16 encoding is equal to the Unicode code point.
    • UTF-32 encoding: Every character uses 4 bytes. This means that UTF-32 takes up more space than UTF-8 and UTF-16, especially for text with a high proportion of ASCII characters.

    From the perspective of storage space occupation, using UTF-8 to represent English characters is very efficient because it only requires 1 byte; using UTF-16 encoding for some non-English characters (such as Chinese) will be more efficient because it only requires 2 bytes, while UTF-8 may require 3 bytes.

    From a compatibility perspective, UTF-8 has the best universality, and many tools and libraries support UTF-8 first.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345-character-encoding-in-programming-languages","level":2,"title":"3.4.5   Character Encoding in Programming Languages","text":"

    For many programming languages in the past, strings during program execution used internal encodings such as UTF-16 or UTF-32. Under these representations, we can often treat strings like arrays during processing, and this approach has the following advantages.

    • Random access: UTF-16 encoded strings can be easily accessed randomly. UTF-8 is a variable-length encoding. To find the \\(i\\)-th character, we need to traverse from the beginning of the string to the \\(i\\)-th character, which requires \\(O(n)\\) time.
    • Character counting: Similar to random access, calculating the length of a UTF-16 encoded string is also an \\(O(1)\\) operation. However, calculating the length of a UTF-8 encoded string requires traversing the entire string.
    • String operations: Many string operations (such as splitting, joining, inserting, deleting, etc.) on UTF-16 encoded strings are easier to perform. Performing these operations on UTF-8 encoded strings usually requires additional calculations to ensure that invalid UTF-8 encoding is not generated.

    In fact, the design of character encoding schemes for programming languages is a very interesting topic involving many factors.

    • Java's String type uses UTF-16 encoding, with each character occupying 2 bytes. This is because at the beginning of Java language design, people believed that 16 bits were sufficient to represent all possible characters. However, this was an incorrect judgment. Later, the Unicode specification expanded beyond 16 bits, so characters in Java may now be represented by a pair of 16-bit values (called \"surrogate pairs\").
    • The strings of JavaScript and TypeScript use UTF-16 encoding for reasons similar to Java. When Netscape first introduced the JavaScript language in 1995, Unicode was still in its early stages of development, and at that time, using 16-bit encoding was sufficient to represent all Unicode characters.
    • C# uses UTF-16 encoding mainly because the .NET platform was designed by Microsoft, and many of Microsoft's technologies (including the Windows operating system) extensively use UTF-16 encoding.

    Due to the underestimation of character quantities by the above programming languages, they had to adopt the \"surrogate pair\" method to represent Unicode characters with lengths exceeding 16 bits. This is a reluctant compromise. On the one hand, in strings containing surrogate pairs, one character may occupy 2 bytes or 4 bytes, thus losing the advantage of fixed-length encoding. On the other hand, handling surrogate pairs requires additional code, which increases the complexity and difficulty of debugging in programming.

    For the above reasons, some programming languages have proposed different encoding schemes.

    • Python's str uses Unicode encoding and adopts a flexible string representation where the stored character length depends on the largest Unicode code point in the string. If all characters in the string are ASCII characters, each character occupies 1 byte; if there are characters exceeding the ASCII range but all within the Basic Multilingual Plane (BMP), each character occupies 2 bytes; if there are characters exceeding the BMP, each character occupies 4 bytes.
    • Go language's string type uses UTF-8 encoding internally. Go language also provides the rune type, which is used to represent a single Unicode code point.
    • Rust language's str and String types use UTF-8 encoding internally. Rust also provides the char type for representing a single Unicode code point.

    It should be noted that the above discussion is about how strings are stored in programming languages, which is different from how strings are stored in files or transmitted over networks. In file storage or network transmission, we usually encode strings into UTF-8 format to achieve optimal compatibility and space efficiency.

    ","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   Classification of Data Structures","text":"

    Common data structures include arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. They can be classified from two dimensions: \"logical structure\" and \"physical structure\".

    ","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311-logical-structure-linear-and-non-linear","level":2,"title":"3.1.1   Logical Structure: Linear and Non-Linear","text":"

    Logical structure reveals the logical relationships between data elements. In arrays and linked lists, data is arranged in a certain order, embodying linear relationships between elements; while in trees, data is arranged hierarchically from top to bottom, showing parent-descendant relationships; graphs are composed of nodes and edges, reflecting complex network relationships.

    As shown in Figure 3-1, logical structures can be divided into two major categories: \"linear\" and \"non-linear\". Linear structures are more intuitive, indicating that data is linearly arranged in logical relationships; non-linear structures are the opposite, arranged non-linearly.

    • Linear data structures: Arrays, linked lists, stacks, queues, hash tables, where elements have a one-to-one sequential relationship.
    • Non-linear data structures: Trees, heaps, graphs, hash tables.

    Non-linear data structures can be further divided into tree structures and network structures.

    • Tree structures: Trees, heaps, hash tables, where elements have a one-to-many relationship.
    • Network structures: Graphs, where elements have a many-to-many relationship.

    Figure 3-1   Linear and non-linear data structures

    ","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312-physical-structure-contiguous-and-dispersed","level":2,"title":"3.1.2   Physical Structure: Contiguous and Dispersed","text":"

    When an algorithm program runs, the data being processed is mainly stored in memory. Figure 3-2 shows a computer memory stick, where each black square contains a memory space. We can imagine memory as a huge Excel spreadsheet, where each cell can store a certain amount of data.

    The system accesses data at the target location through memory addresses. As shown in Figure 3-2, the computer assigns a number to each cell in the spreadsheet according to specific rules, ensuring that each memory space has a unique memory address. With these addresses, the program can access data in memory.

    Figure 3-2   Memory stick, memory space, memory address

    Tip

    It should be noted that comparing memory to an Excel spreadsheet is only a simplified analogy. The actual workings of memory are much more complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory.

    Memory is a shared resource for all programs. When a block of memory is occupied by a program, it usually cannot be used by other programs at the same time. Therefore, in the design of data structures and algorithms, memory resources are an important consideration. For example, the peak memory occupied by an algorithm should not exceed the remaining free memory of the system; if there is a lack of contiguous large memory blocks, then the data structure chosen must be able to be stored in dispersed memory spaces.

    As shown in Figure 3-3, physical structure reflects the way data is stored in computer memory. It can be divided into contiguous-space storage (arrays) and dispersed-space storage (linked lists). At a low level, physical structure determines how data is accessed, updated, inserted, and deleted. These two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency.

    Figure 3-3   Contiguous space storage and dispersed space storage

    It is worth noting that all data structures are implemented based on arrays, linked lists, or a combination of both. For example, stacks and queues can be implemented using either arrays or linked lists; while the implementation of hash tables may include both arrays and linked lists.

    • Can be implemented based on arrays: Stacks, queues, hash tables, trees, heaps, graphs, matrices, tensors (arrays with dimensions \\(\\geq 3\\)), etc.
    • Can be implemented based on linked lists: Stacks, queues, hash tables, trees, heaps, graphs, etc.

    After initialization, linked lists can still adjust their length during program execution, so they are also called \"dynamic data structures\". After initialization, the length of arrays cannot be changed, so they are also called \"static data structures\". It is worth noting that arrays can change length by reallocating memory, thus retaining a limited degree of flexibility.

    Tip

    If you find it difficult to understand physical structure, it is recommended to read the next chapter first, and then review this section.

    ","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   Number Encoding *","text":"

    Tip

    In this book, chapters marked with an asterisk * are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters.

    ","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331-sign-magnitude-1s-complement-and-2s-complement","level":2,"title":"3.3.1   Sign-Magnitude, 1's Complement, and 2's Complement","text":"

    In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the byte range is \\([-128, 127]\\). This phenomenon is counterintuitive, and its underlying cause lies in sign-magnitude, 1's complement, and 2's complement representations.

    First, it should be noted that numbers are stored in computers in the form of \"2's complement\". Before analyzing the reasons for this, let's first define these three concepts.

    • Sign-magnitude: We treat the highest bit of the binary representation of a number as the sign bit, where \\(0\\) represents a positive number and \\(1\\) represents a negative number, and the remaining bits represent the value of the number.
    • 1's complement: The 1's complement of a positive number is the same as its sign-magnitude. For a negative number, the 1's complement is obtained by inverting all bits except the sign bit of its sign-magnitude.
    • 2's complement: The 2's complement of a positive number is the same as its sign-magnitude. For a negative number, the 2's complement is obtained by adding \\(1\\) to its 1's complement.

    Figure 3-4 shows the conversion methods among sign-magnitude, 1's complement, and 2's complement.

    Figure 3-4   Conversions among sign-magnitude, 1's complement, and 2's complement

    Sign-magnitude, although the most intuitive, has some limitations. On one hand, the sign-magnitude of negative numbers cannot be directly used in operations. For example, calculating \\(1 + (-2)\\) in sign-magnitude yields \\(-3\\), which is clearly incorrect.

    \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

    To solve this problem, computers introduced 1's complement. If we first convert sign-magnitude to 1's complement and calculate \\(1 + (-2)\\) in 1's complement, then convert the result back to sign-magnitude, we can obtain the correct result of \\(-1\\).

    \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(Sign-magnitude)} + 1000 \\; 0010 \\; \\text{(Sign-magnitude)} \\newline & = 0000 \\; 0001 \\; \\text{(1's complement)} + 1111 \\; 1101 \\; \\text{(1's complement)} \\newline & = 1111 \\; 1110 \\; \\text{(1's complement)} \\newline & = 1000 \\; 0001 \\; \\text{(Sign-magnitude)} \\newline & \\rightarrow -1 \\end{aligned} \\]

    On the other hand, the sign-magnitude of the number zero has two representations, \\(+0\\) and \\(-0\\). This means that the number zero corresponds to two different binary encodings, which may cause ambiguity. For example, in conditional judgments, if we don't distinguish between positive zero and negative zero, it may lead to incorrect judgment results. If we want to handle the ambiguity of positive and negative zero, we need to introduce additional judgment operations, which may reduce the computational efficiency of the computer.

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

    Like sign-magnitude, 1's complement also has the problem of positive and negative zero ambiguity. Therefore, computers further introduced 2's complement. Let's first observe the conversion process of negative zero from sign-magnitude to 1's complement to 2's complement:

    \\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(Sign-magnitude)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(1's complement)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(2's complement)} \\newline \\end{aligned} \\]

    Adding \\(1\\) to the 1's complement of negative zero produces a carry, but since the byte type has a length of only 8 bits, the \\(1\\) that overflows to the 9th bit is discarded. That is to say, the 2's complement of negative zero is \\(0000 \\; 0000\\), which is the same as the 2's complement of positive zero. This means that in 2's complement representation, there is only one zero, and the positive and negative zero ambiguity is thus resolved.

    One last question remains: the range of the byte type is \\([-128, 127]\\), so where does the extra negative number \\(-128\\) come from? We notice that all integers in the interval \\([-127, +127]\\) have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other.

    However, the 2's complement \\(1000 \\; 0000\\) is an exception, and it does not have a corresponding sign-magnitude. According to the conversion method, we get that the sign-magnitude of this 2's complement is \\(0000 \\; 0000\\). This is clearly contradictory because this sign-magnitude represents the number \\(0\\), and its 2's complement should be itself. The computer specifies that this special 2's complement \\(1000 \\; 0000\\) represents \\(-128\\). In fact, the result of calculating \\((-1) + (-127)\\) in 2's complement is \\(-128\\).

    \\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(Sign-magnitude)} + 1000 \\; 0001 \\; \\text{(Sign-magnitude)} \\newline & = 1000 \\; 0000 \\; \\text{(1's complement)} + 1111 \\; 1110 \\; \\text{(1's complement)} \\newline & = 1000 \\; 0001 \\; \\text{(2's complement)} + 1111 \\; 1111 \\; \\text{(2's complement)} \\newline & = 1000 \\; 0000 \\; \\text{(2's complement)} \\newline & \\rightarrow -128 \\end{aligned} \\]

    You may have noticed that all the above calculations are addition operations. This hints at an important fact: the hardware circuits inside computers are mainly designed based on addition operations. This is because addition operations are simpler to implement in hardware compared to other operations (such as multiplication, division, and subtraction), easier to parallelize, and have faster operation speeds.

    Please note that this does not mean that computers can only perform addition. By combining addition with some basic logical operations, computers can implement various other mathematical operations. For example, calculating the subtraction \\(a - b\\) can be converted to calculating the addition \\(a + (-b)\\); calculating multiplication and division can be converted to calculating multiple additions or subtractions.

    We can now summarize why computers use 2's complement: with 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without designing special hardware circuits for subtraction or separately handling the ambiguity of positive and negative zero. This greatly simplifies hardware design and improves efficiency.

    The design of 2's complement is very ingenious. Due to space limitations, we will stop here. Interested readers are encouraged to explore further.

    ","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332-floating-point-number-encoding","level":2,"title":"3.3.2   Floating-Point Number Encoding","text":"

    Careful readers may have noticed: int and float have the same length, both are 4 bytes, but why does float have a much larger range than int? This is very counterintuitive because it stands to reason that float needs to represent decimals, so the range should be smaller.

    In fact, this is because floating-point number float uses a different representation method. Let's denote a 32-bit binary number as:

    \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

    According to the IEEE 754 standard, a 32-bit float consists of the following three parts.

    • Sign bit \\(\\mathrm{S}\\): occupies 1 bit, corresponding to \\(b_{31}\\).
    • Exponent bit \\(\\mathrm{E}\\): occupies 8 bits, corresponding to \\(b_{30} b_{29} \\ldots b_{23}\\).
    • Fraction bit \\(\\mathrm{N}\\): occupies 23 bits, corresponding to \\(b_{22} b_{21} \\ldots b_0\\).

    The calculation method for the value corresponding to the binary float is:

    \\[ \\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 \\]

    Converted to decimal, the calculation formula is:

    \\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\]

    The range of each component is:

    \\[ \\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} \\]

    Figure 3-5   Calculation example of float under IEEE 754 standard

    Observing Figure 3-5, given example data \\(\\mathrm{S} = 0\\), \\(\\mathrm{E} = 124\\), \\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\), we have:

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

    Now we can answer the initial question: the representation of float includes an exponent bit, resulting in a range far greater than int. According to the above calculation, the maximum positive number that float can represent is \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\), and the minimum negative number can be obtained by switching the sign bit.

    Although floating-point number float expands the range, its side effect is sacrificing precision. The integer type int uses all 32 bits to represent numbers, and the numbers are evenly distributed; however, due to the existence of the exponent bit, the larger the value of floating-point number float, the larger the difference between two adjacent numbers tends to be.

    As shown in Table 3-2, exponent bits \\(\\mathrm{E} = 0\\) and \\(\\mathrm{E} = 255\\) have special meanings, used to represent zero, infinity, \\(\\mathrm{NaN}\\), etc.

    Table 3-2   Meaning of exponent bits

    Exponent Bit E Fraction Bit \\(\\mathrm{N} = 0\\) Fraction Bit \\(\\mathrm{N} \\ne 0\\) Calculation Formula \\(0\\) \\(\\pm 0\\) Subnormal Number \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) Normal Number Normal Number \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\)

    It is worth noting that subnormal numbers significantly improve the precision of floating-point numbers. The smallest positive normal number is \\(2^{-126}\\), and the smallest positive subnormal number is \\(2^{-126} \\times 2^{-23}\\).

    Double-precision double also uses a representation method similar to float, which will not be elaborated here.

    ","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   Summary","text":"","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_data_structure/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Data structures can be classified from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory.
    • Common logical structures include linear, tree-like, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures.
    • When a program runs, data is stored in computer memory. Each memory space has a corresponding memory address, and the program accesses data through these memory addresses.
    • Physical structures are primarily divided into contiguous space storage (arrays) and dispersed space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both.
    • Basic data types in computers include integers byte, short, int, long, floating-point numbers float, double, characters char, and booleans bool. Their value ranges depend on the size of space they occupy and their representation method.
    • Sign-magnitude, 1's complement, and 2's complement are three methods for encoding numbers in computers, and they can be converted into each other. The most significant bit of sign-magnitude is the sign bit, and the remaining bits represent the value of the number.
    • Integers are stored in computers in 2's complement form. Under 2's complement representation, computers can treat the addition of positive and negative numbers uniformly, without needing to design special hardware circuits for subtraction, and there is no ambiguity of positive and negative zero.
    • The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bits, the range of floating-point numbers is much larger than that of integers, at the cost of sacrificing precision.
    • ASCII is the earliest English character set, with a length of 1 byte, containing a total of 128 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods.
    • UTF-8 is the most popular Unicode encoding method and has excellent compatibility. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are common Unicode encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default.
    ","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Why do hash tables contain both linear and non-linear data structures?

    The underlying structure of a hash table is an array. To resolve hash collisions, we may use \"chaining\" (discussed in the subsequent \"Hash Collision\" section): each bucket in the array points to a linked list, which may be converted to a tree (usually a red-black tree) when the list length exceeds a certain threshold.

    From a storage perspective, the underlying structure of a hash table is an array, where each bucket slot may contain a value, a linked list, or a tree. Therefore, hash tables may contain both linear data structures (arrays, linked lists) and non-linear data structures (trees).

    Q: Is the length of the char type 1 byte?

    The length of the char type is determined by the encoding method used by the programming language. For example, Java, JavaScript, TypeScript, and C# all use UTF-16 encoding (to store Unicode code points), so the char type has a length of 2 bytes.

    Q: Is there ambiguity in referring to array-based data structures as \"static data structures\"? Stacks can also perform \"dynamic\" operations such as push and pop.

    Stacks can indeed implement dynamic data operations, but the data structure is still \"static\" (fixed length). Although array-based data structures can dynamically add or remove elements, their capacity is fixed. If the data volume exceeds the pre-allocated size, a new larger array needs to be created, and the contents of the old array must be copied to the new array.

    Q: When constructing a stack (queue), its size is not specified. Why are they \"static data structures\"?

    In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); the class handles this automatically. For example, the initial capacity of Java's ArrayList is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent \"List\" section for details.

    Q: The method of converting sign-magnitude to 2's complement is \"first negate then add 1\". So converting 2's complement to sign-magnitude should be the inverse operation \"first subtract 1 then negate\". However, 2's complement can also be converted to sign-magnitude through \"first negate then add 1\". Why is this?

    This is because the mutual conversion between sign-magnitude and 2's complement is actually the process of computing the \"complement\". Let us first define the complement: assuming \\(a + b = c\\), then we say that \\(a\\) is the complement of \\(b\\) to \\(c\\), and conversely, \\(b\\) is the complement of \\(a\\) to \\(c\\).

    Given an \\(n = 4\\) bit binary number \\(0010\\), if we treat this number as sign-magnitude (ignoring the sign bit), then its 2's complement can be obtained through \"first negate then add 1\":

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

    We find that the sum of sign-magnitude and 2's complement is \\(0010 + 1110 = 10000\\), which means the 2's complement \\(1110\\) is the \"complement\" of sign-magnitude \\(0010\\) to \\(10000\\). This means the above \"first negate then add 1\" is actually the process of computing the complement to \\(10000\\).

    So, what is the \"complement\" of 2's complement \\(1110\\) to \\(10000\\)? We can still use \"first negate then add 1\" to obtain it:

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

    In other words, sign-magnitude and 2's complement are each other's \"complement\" to \\(10000\\), so \"sign-magnitude to 2's complement\" and \"2's complement to sign-magnitude\" can be implemented using the same operation (first negate then add 1).

    Of course, we can also use the inverse operation to find the sign-magnitude of 2's complement \\(1110\\), that is, \"first subtract 1 then negate\":

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

    In summary, both \"first negate then add 1\" and \"first subtract 1 then negate\" are computing the complement to \\(10000\\), and they are equivalent.

    Essentially, the \"negate\" operation is actually finding the complement to \\(1111\\) (because \"sign-magnitude + 1's complement = 1111\" always holds); and adding 1 to the 1's complement yields the 2's complement, which is the complement to \\(10000\\).

    The above uses \\(n = 4\\) as an example, and it can be generalized to binary numbers of any number of bits.

    ","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"Chapter 12.   Divide and Conquer","text":"

    Abstract

    Difficult problems are decomposed layer by layer, with each decomposition making them simpler.

    Divide and conquer reveals an important truth: start with what is simple, and nothing remains complex.

    ","path":["Chapter 12. Divide and Conquer","Chapter 12.   Divide and Conquer"],"tags":[]},{"location":"chapter_divide_and_conquer/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 12.1   Divide and Conquer Algorithms
    • 12.2   Divide and Conquer Search Strategy
    • 12.3   Building a Binary Tree Problem
    • 12.4   Hanota Problem
    • 12.5   Summary
    ","path":["Chapter 12. Divide and Conquer","Chapter 12.   Divide and Conquer"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   Divide and Conquer Search Strategy","text":"

    We have already learned that search algorithms are divided into two major categories.

    • Brute-force search: Implemented by traversing the data structure, with a time complexity of \\(O(n)\\).
    • Adaptive search: Leverages specific data organization or prior information, with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\).

    In fact, search algorithms with time complexity of \\(O(\\log n)\\) are typically implemented based on the divide and conquer strategy, such as binary search and trees.

    • Each step of binary search divides the problem (searching for a target element in an array) into a smaller problem (searching for the target element in half of the array), continuing until the array is empty or the target element is found.
    • Trees are representative of the divide and conquer idea. In data structures such as binary search trees, AVL trees, and heaps, the time complexity of various operations is \\(O(\\log n)\\).

    The divide and conquer strategy of binary search is as follows.

    • The problem can be decomposed: Binary search recursively decomposes the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element.
    • Subproblems are independent: In binary search, each round only processes one subproblem, which is not affected by other subproblems.
    • Solutions of subproblems do not need to be merged: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved.

    Divide and conquer can improve search efficiency because brute-force search can only eliminate one option per round, while divide and conquer search can eliminate half of the options per round.

    ","path":["Chapter 12. Divide and Conquer","12.2   Divide and Conquer Search Strategy"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1-implementing-binary-search-based-on-divide-and-conquer","level":3,"title":"1.   Implementing Binary Search Based on Divide and Conquer","text":"

    In previous sections, binary search was implemented based on iteration. Now we implement it based on divide and conquer (recursion).

    Question

    Given a sorted array nums of length \\(n\\), where all elements are unique, find target.

    From a divide and conquer perspective, we denote the subproblem corresponding to the search interval \\([i, j]\\) as \\(f(i, j)\\).

    Starting from the original problem \\(f(0, n-1)\\), perform binary search through the following steps.

    1. Calculate the midpoint \\(m\\) of the search interval \\([i, j]\\), and use it to eliminate half of the search interval.
    2. Recursively solve the subproblem reduced by half in size, which could be \\(f(i, m-1)\\) or \\(f(m+1, j)\\).
    3. Repeat steps 1. and 2. until target is found, or return when the interval is empty.

    Figure 12-4 shows the divide and conquer process of binary search for element \\(6\\) in an array.

    Figure 12-4   Divide and conquer process of binary search

    In the implementation code, we declare a recursive function dfs() to solve the problem \\(f(i, j)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py
    def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n    \"\"\"Binary search: problem f(i, j)\"\"\"\n    # If the interval is empty, it means there is no target element, return -1\n    if i > j:\n        return -1\n    # Calculate the midpoint index m\n    m = (i + j) // 2\n    if nums[m] < target:\n        # Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j)\n    elif nums[m] > target:\n        # Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1)\n    else:\n        # Found the target element, return its index\n        return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Binary search\"\"\"\n    n = len(nums)\n    # Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n
    binary_search_recur.cpp
    /* Binary search: problem f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(vector<int> &nums, int target) {\n    int n = nums.size();\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.java
    /* Binary search: problem f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(int[] nums, int target) {\n    int n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.cs
    /* Binary search: problem f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return DFS(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return DFS(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint BinarySearch(int[] nums, int target) {\n    int n = nums.Length;\n    // Solve the problem f(0, n-1)\n    return DFS(nums, target, 0, n - 1);\n}\n
    binary_search_recur.go
    /* Binary search: problem f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n    // If interval is empty, indicating no target element, return -1\n    if i > j {\n        return -1\n    }\n    // Calculate midpoint index\n    m := i + ((j - i) >> 1)\n    // Compare midpoint with target element\n    if nums[m] < target {\n        // If smaller, recurse on right half of array\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m+1, j)\n    } else if nums[m] > target {\n        // If larger, recurse on left half of array\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m-1)\n    } else {\n        // Found the target element, return its index\n        return m\n    }\n}\n\n/* Binary search */\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
    /* Binary search: problem f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {\n    // If the interval is empty, it means there is no target element, return -1\n    if i > j {\n        return -1\n    }\n    // Calculate the midpoint index m\n    let m = (i + j) / 2\n    if nums[m] < target {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums: nums, target: target, i: m + 1, j: j)\n    } else if nums[m] > target {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums: nums, target: target, i: i, j: m - 1)\n    } else {\n        // Found the target element, return its index\n        return m\n    }\n}\n\n/* Binary search */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Solve the problem f(0, n-1)\n    dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n
    binary_search_recur.js
    /* Binary search: problem f(i, j) */\nfunction dfs(nums, target, i, j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfunction binarySearch(nums, target) {\n    const n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.ts
    /* Binary search: problem f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfunction binarySearch(nums: number[], target: number): number {\n    const n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.dart
    /* Binary search: problem f(i, j) */\nint dfs(List<int> nums, int target, int i, int j) {\n  // If the interval is empty, it means there is no target element, return -1\n  if (i > j) {\n    return -1;\n  }\n  // Calculate the midpoint index m\n  int m = (i + j) ~/ 2;\n  if (nums[m] < target) {\n    // Recursion subproblem f(m+1, j)\n    return dfs(nums, target, m + 1, j);\n  } else if (nums[m] > target) {\n    // Recursion subproblem f(i, m-1)\n    return dfs(nums, target, i, m - 1);\n  } else {\n    // Found the target element, return its index\n    return m;\n  }\n}\n\n/* Binary search */\nint binarySearch(List<int> nums, int target) {\n  int n = nums.length;\n  // Solve the problem f(0, n-1)\n  return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.rs
    /* Binary search: problem f(i, j) */\nfn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {\n    // If the interval is empty, it means there is no target element, return -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        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if nums[m as usize] > target {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    let n = nums.len() as i32;\n    // Solve the problem f(0, n-1)\n    dfs(nums, target, 0, n - 1)\n}\n
    binary_search_recur.c
    /* Binary search: problem f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(int nums[], int target, int numsSize) {\n    int n = numsSize;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
    binary_search_recur.kt
    /* Binary search: problem f(i, j) */\nfun dfs(\n    nums: IntArray,\n    target: Int,\n    i: Int,\n    j: Int\n): Int {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1\n    }\n    // Calculate the midpoint index m\n    val m = (i + j) / 2\n    return if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        dfs(nums, target, m + 1, j)\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        dfs(nums, target, i, m - 1)\n    } else {\n        // Found the target element, return its index\n        m\n    }\n}\n\n/* Binary search */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    val n = nums.size\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n}\n
    binary_search_recur.rb
    ### Binary search: problem f(i, j) ###\ndef dfs(nums, target, i, j)\n  # If the interval is empty, it means there is no target element, return -1\n  return -1 if i > j\n\n  # Calculate the midpoint index m\n  m = (i + j) / 2\n\n  if nums[m] < target\n    # Recursion subproblem f(m+1, j)\n    return dfs(nums, target, m + 1, j)\n  elsif nums[m] > target\n    # Recursion subproblem f(i, m-1)\n    return dfs(nums, target, i, m - 1)\n  else\n    # Found the target element, return its index\n    return m\n  end\nend\n\n### Binary search ###\ndef binary_search(nums, target)\n  n = nums.length\n  # Solve the problem f(0, n-1)\n  dfs(nums, target, 0, n - 1)\nend\n
    ","path":["Chapter 12. Divide and Conquer","12.2   Divide and Conquer Search Strategy"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   Building a Binary Tree Problem","text":"

    Question

    Given the preorder traversal preorder and inorder traversal inorder of a binary tree, construct the binary tree and return the root node of the binary tree. Assume there are no duplicate node values in the binary tree (as shown in Figure 12-5).

    Figure 12-5   Example data for building a binary tree

    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1-determining-if-it-is-a-divide-and-conquer-problem","level":3,"title":"1.   Determining If It Is a Divide and Conquer Problem","text":"

    The original problem is defined as constructing a binary tree from preorder and inorder, which is a typical divide and conquer problem.

    • The problem can be decomposed: From a divide and conquer perspective, we can divide the original problem into two subproblems: constructing the left subtree and constructing the right subtree, plus one operation: initializing the root node. For each subtree (subproblem), we can still reuse the above division method, dividing it into smaller subtrees (subproblems) until the smallest subproblem (empty subtree) is reached.
    • Subproblems are independent: The left and right subtrees are independent of each other; there is no overlap between them. When constructing the left subtree, we only need to focus on the parts of the inorder and preorder traversals corresponding to the left subtree. The same applies to the right subtree.
    • Solutions of subproblems can be merged: Once we have the left and right subtrees (solutions of subproblems), we can link them to the root node to obtain the solution to the original problem.
    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2-how-to-divide-subtrees","level":3,"title":"2.   How to Divide Subtrees","text":"

    Based on the above analysis, this problem can be solved using divide and conquer, but how do we divide the left and right subtrees through the preorder traversal preorder and inorder traversal inorder?

    According to the definition, both preorder and inorder can be divided into three parts.

    • Preorder traversal: [ Root Node | Left Subtree | Right Subtree ], for example, the tree in Figure 12-5 corresponds to [ 3 | 9 | 2 1 7 ].
    • Inorder traversal: [ Left Subtree | Root Node | Right Subtree ], for example, the tree in Figure 12-5 corresponds to [ 9 | 3 | 1 2 7 ].

    Using the data from the figure above as an example, we can obtain the division results through the steps shown in Figure 12-6.

    1. The first element 3 in the preorder traversal is the value of the root node.
    2. Find the index of root node 3 in inorder, and use this index to divide inorder into [ 9 | 3 | 1 2 7 ].
    3. Based on the division result of inorder, it is easy to determine that the left and right subtrees have 1 and 3 nodes respectively, allowing us to divide preorder into [ 3 | 9 | 2 1 7 ].

    Figure 12-6   Dividing subtrees in preorder and inorder traversals

    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3-describing-subtree-intervals-based-on-variables","level":3,"title":"3.   Describing Subtree Intervals Based on Variables","text":"

    Based on the above division method, we have obtained the index intervals of the root node, left subtree, and right subtree in preorder and inorder. To describe these index intervals, we need to use several index variables.

    • Denote the index of the current tree's root node in preorder as \\(i\\).
    • Denote the index of the current tree's root node in inorder as \\(m\\).
    • Denote the index interval of the current tree in inorder as \\([l, r]\\).

    As shown in Table 12-1, through these variables we can represent the index of the root node in preorder and the index intervals of the subtrees in inorder.

    Table 12-1   Indices of root node and subtrees in preorder and inorder traversals

    Root node index in preorder Subtree index interval in inorder Current tree \\(i\\) \\([l, r]\\) Left subtree \\(i + 1\\) \\([l, m-1]\\) Right subtree \\(i + 1 + (m - l)\\) \\([m+1, r]\\)

    Please note that \\((m-l)\\) in the right subtree root node index means \"the number of nodes in the left subtree\". It is recommended to understand this in conjunction with Figure 12-7.

    Figure 12-7   Index interval representation of root node and left and right subtrees

    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4-code-implementation","level":3,"title":"4.   Code Implementation","text":"

    To improve the efficiency of querying \\(m\\), we use a hash table hmap to store the mapping from elements in the inorder array to their indices:

    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    \"\"\"Build binary tree: divide and conquer\"\"\"\n    # Terminate when the subtree interval is empty\n    if r - l < 0:\n        return None\n    # Initialize the root node\n    root = TreeNode(preorder[i])\n    # Query m to divide the left and right subtrees\n    m = inorder_map[preorder[i]]\n    # Subproblem: build the left subtree\n    root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n    # Subproblem: build the right subtree\n    root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n    # Return the root node\n    return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n    \"\"\"Build binary tree\"\"\"\n    # Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return NULL;\n    // Initialize the root node\n    TreeNode *root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return null;\n    // Initialize the root node\n    TreeNode root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return null;\n    // Initialize the root node\n    TreeNode root = new(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n    // Terminate when the subtree interval is empty\n    if r-l < 0 {\n        return nil\n    }\n    // Initialize the root node\n    root := NewTreeNode(preorder[i])\n    // Query m to divide the left and right subtrees\n    m := inorderMap[preorder[i]]\n    // Subproblem: build the left subtree\n    root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n    // Subproblem: build the right subtree\n    root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {\n    // Terminate when the subtree interval is empty\n    if r - l < 0 {\n        return nil\n    }\n    // Initialize the root node\n    let root = TreeNode(x: preorder[i])\n    // Query m to divide the left and right subtrees\n    let m = inorderMap[preorder[i]]!\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfunc buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfunction dfs(preorder, inorderMap, i, l, r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null;\n    // Initialize the root node\n    const root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    const m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nfunction buildTree(preorder, inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfunction dfs(\n    preorder: number[],\n    inorderMap: Map<number, number>,\n    i: number,\n    l: number,\n    r: number\n): TreeNode | null {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null;\n    // Initialize the root node\n    const root: TreeNode = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    const m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode? dfs(\n  List<int> preorder,\n  Map<int, int> inorderMap,\n  int i,\n  int l,\n  int r,\n) {\n  // Terminate when the subtree interval is empty\n  if (r - l < 0) {\n    return null;\n  }\n  // Initialize the root node\n  TreeNode? root = TreeNode(preorder[i]);\n  // Query m to divide the left and right subtrees\n  int m = inorderMap[preorder[i]]!;\n  // Subproblem: build the left subtree\n  root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n  // Subproblem: build the right subtree\n  root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n  // Return the root node\n  return root;\n}\n\n/* Build binary tree */\nTreeNode? buildTree(List<int> preorder, List<int> inorder) {\n  // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\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    // Terminate when the subtree interval is empty\n    if r - l < 0 {\n        return None;\n    }\n    // Initialize the root node\n    let root = TreeNode::new(preorder[i as usize]);\n    // Query m to divide the left and right subtrees\n    let m = inorder_map.get(&preorder[i as usize]).unwrap();\n    // Subproblem: build the left subtree\n    root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    Some(root)\n}\n\n/* Build binary tree */\nfn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return NULL;\n    // Initialize the root node\n    TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n    root->val = preorder[i];\n    root->left = NULL;\n    root->right = NULL;\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n    // Subproblem: build the right subtree\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    /* Build binary tree: divide and conquer */\nfun dfs(\n    preorder: IntArray,\n    inorderMap: Map<Int?, Int?>,\n    i: Int,\n    l: Int,\n    r: Int\n): TreeNode? {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null\n    // Initialize the root node\n    val root = TreeNode(preorder[i])\n    // Query m to divide the left and right subtrees\n    val m = inorderMap[preorder[i]]!!\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n    // Initialize hash map, storing the mapping from inorder elements to indices\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
    ### Build binary tree: divide and conquer ###\ndef dfs(preorder, inorder_map, i, l, r)\n  # Terminate when the subtree interval is empty\n  return if r - l < 0\n\n  # Initialize the root node\n  root = TreeNode.new(preorder[i])\n  # Query m to divide the left and right subtrees\n  m = inorder_map[preorder[i]]\n  # Subproblem: build the left subtree\n  root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n  # Subproblem: build the right subtree\n  root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n  # Return the root node\n  root\nend\n\n### Build binary tree ###\ndef build_tree(preorder, inorder)\n  # Initialize hash map, storing the mapping from inorder elements to indices\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

    Figure 12-8 shows the recursive process of building the binary tree. Each node is established during the downward \"recursion\" process, while each edge (reference) is established during the upward \"return\" process.

    <1><2><3><4><5><6><7><8><9>

    Figure 12-8   Recursive process of building a binary tree

    The division results of the preorder traversal preorder and inorder traversal inorder within each recursive function are shown in Figure 12-9.

    Figure 12-9   Division results in each recursive function

    Let the number of nodes in the tree be \\(n\\). Initializing each node (executing one recursive function dfs()) takes \\(O(1)\\) time. Therefore, the overall time complexity is \\(O(n)\\).

    The hash table stores the mapping from inorder elements to their indices, with a space complexity of \\(O(n)\\). In the worst case, when the binary tree degenerates into a linked list, the recursion depth reaches \\(n\\), using \\(O(n)\\) stack frame space. Therefore, the overall space complexity is \\(O(n)\\).

    ","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   Divide and Conquer Algorithms","text":"

    Divide and conquer is a very important and common algorithmic strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: \"divide\" and \"conquer\".

    1. Divide (partition phase): Recursively divide the original problem into two or more subproblems until the smallest subproblem is reached.
    2. Conquer (merge phase): Starting from the smallest subproblems with known solutions, merge the solutions of subproblems from bottom to top to construct the solution to the original problem.

    As shown in Figure 12-1, \"merge sort\" is one of the typical applications of the divide and conquer strategy.

    1. Divide: Recursively divide the original array (original problem) into two subarrays (subproblems) until the subarray has only one element (smallest subproblem).
    2. Conquer: Merge the sorted subarrays (solutions to subproblems) from bottom to top to obtain a sorted original array (solution to the original problem).

    Figure 12-1   Divide and conquer strategy of merge sort

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211-how-to-determine-divide-and-conquer-problems","level":2,"title":"12.1.1   How to Determine Divide and Conquer Problems","text":"

    Whether a problem is suitable for solving with divide and conquer can usually be determined based on the following criteria.

    1. The problem can be decomposed: The original problem can be divided into smaller, similar subproblems, and can be recursively divided in the same way.
    2. Subproblems are independent: There is no overlap between subproblems, they are independent of each other and can be solved independently.
    3. Solutions of subproblems can be merged: The solution to the original problem is obtained by merging the solutions of subproblems.

    Clearly, merge sort satisfies these three criteria.

    1. The problem can be decomposed: Recursively divide the array (original problem) into two subarrays (subproblems).
    2. Subproblems are independent: Each subarray can be sorted independently (subproblems can be solved independently).
    3. Solutions of subproblems can be merged: Two sorted subarrays (solutions of subproblems) can be merged into one sorted array (solution of the original problem).
    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212-improving-efficiency-through-divide-and-conquer","level":2,"title":"12.1.2   Improving Efficiency Through Divide and Conquer","text":"

    Divide and conquer can not only effectively solve algorithmic problems, but can often also improve algorithmic efficiency. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy.

    This raises the question: Why can divide and conquer improve algorithm efficiency, and what is the underlying logic? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation.

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1-operation-count-optimization","level":3,"title":"1.   Operation Count Optimization","text":"

    Taking \"bubble sort\" as an example, processing an array of length \\(n\\) requires \\(O(n^2)\\) time. Suppose we divide the array at the midpoint into two subarrays, as shown in Figure 12-2. The division requires \\(O(n)\\) time, sorting each subarray requires \\(O((n / 2)^2)\\) time, and merging the two subarrays requires \\(O(n)\\) time, resulting in an overall time complexity of:

    \\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\]

    Figure 12-2   Bubble sort before and after array division

    Next, we compute the following inequality, where the left and right sides represent the total number of operations before and after division, respectively:

    \\[ \\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} \\]

    This means that when \\(n > 4\\), the number of operations after division is smaller, and sorting efficiency should be higher. Note that the time complexity after division is still quadratic \\(O(n^2)\\), but the constant term in the complexity has become smaller.

    Going further, what if we continuously divide the subarrays from their midpoints into two subarrays until the subarrays have only one element? This approach is actually \"merge sort\", with a time complexity of \\(O(n \\log n)\\).

    Thinking further, what if we set multiple division points and evenly divide the original array into \\(k\\) subarrays? This situation is very similar to \"bucket sort\", which is well-suited for sorting massive amounts of data, with a theoretical time complexity of \\(O(n + k)\\).

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2-parallel-computation-optimization","level":3,"title":"2.   Parallel Computation Optimization","text":"

    We know that the subproblems generated by divide and conquer are independent of each other, so they can typically be solved in parallel. This means divide and conquer can not only reduce the time complexity of algorithms, but is also amenable to parallel optimization by the operating system.

    Parallel optimization is particularly effective in multi-core or multi-processor environments, as the system can simultaneously handle multiple subproblems, making fuller use of computing resources and significantly reducing overall runtime.

    For example, in the \"bucket sort\" shown in Figure 12-3, we evenly distribute massive data into various buckets, and the sorting tasks for all buckets can be distributed to various computing units. After completion, the results are merged.

    Figure 12-3   Parallel computation in bucket sort

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213-common-applications-of-divide-and-conquer","level":2,"title":"12.1.3   Common Applications of Divide and Conquer","text":"

    On the one hand, divide and conquer can be used to solve many classic algorithmic problems.

    • Finding the closest pair of points: This algorithm first divides the point set into two parts, then finds the closest pair of points in each part separately, and finally finds the closest pair of points that spans both parts.
    • Large integer multiplication: For example, the Karatsuba algorithm, which decomposes large integer multiplication into several smaller integer multiplications and additions.
    • Matrix multiplication: For example, the Strassen algorithm, which decomposes large matrix multiplication into multiple small matrix multiplications and additions.
    • Hanota problem: The hanota problem can be solved through recursion, which is a typical application of the divide and conquer strategy.
    • Solving inversion pairs: In a sequence, if a preceding number is greater than a following number, these two numbers form an inversion pair. Solving the inversion pair problem can utilize the divide and conquer approach with the help of merge sort.

    On the other hand, divide and conquer is widely applied in the design of algorithms and data structures.

    • Binary search: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary-search step on the remaining interval.
    • Merge sort: Already introduced at the beginning of this section, no further elaboration needed.
    • Quick sort: Quick sort selects a pivot value, then divides the array into two subarrays, one with elements smaller than the pivot and the other with elements larger than the pivot, then performs the same division operation on these two parts until the subarrays have only one element.
    • Bucket sort: The basic idea of bucket sort is to scatter data into multiple buckets, then sort the elements within each bucket, and finally extract the elements from each bucket in sequence to obtain a sorted array.
    • Trees: For example, binary search trees, AVL trees, red-black trees, B-trees, B+ trees, etc. Their search, insertion, and deletion operations can all be viewed as applications of the divide and conquer strategy.
    • Heaps: A heap is a special complete binary tree, and its various operations, such as insertion, deletion, and heapify, actually imply the divide and conquer idea.
    • Hash tables: Although hash tables do not directly apply divide and conquer, some methods for resolving hash collisions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve lookup efficiency.

    It can be seen that divide and conquer is a \"quietly pervasive\" algorithmic idea, embedded in various algorithms and data structures.

    ","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   Hanota Problem","text":"

    In merge sort and building binary trees, we decompose the original problem into two subproblems, each half the size of the original problem. However, for the hanota problem, we adopt a different decomposition strategy.

    Question

    Given three pillars, denoted as A, B, and C. Initially, pillar A has \\(n\\) discs stacked on it, arranged from top to bottom in ascending order of size. Our task is to move these \\(n\\) discs to pillar C while maintaining their original order (as shown in Figure 12-10). The following rules must be followed when moving the discs.

    1. A disc can only be taken from the top of one pillar and placed on top of another pillar.
    2. Only one disc can be moved at a time.
    3. A smaller disc must always be on top of a larger disc.

    Figure 12-10   Example of the hanota problem

    We denote the hanota problem of size \\(i\\) as \\(f(i)\\). For example, \\(f(3)\\) represents moving \\(3\\) discs from A to C.

    ","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1-considering-the-base-cases","level":3,"title":"1.   Considering the Base Cases","text":"

    As shown in Figure 12-11, for problem \\(f(1)\\), when there is only one disc, we can move it directly from A to C.

    <1><2>

    Figure 12-11   Solution for a problem of size 1

    As shown in Figure 12-12, for problem \\(f(2)\\), when there are two discs, since we must always keep the smaller disc on top of the larger disc, we need to use B to assist in the move.

    1. First, move the smaller disc from A to B.
    2. Then move the larger disc from A to C.
    3. Finally, move the smaller disc from B to C.
    <1><2><3><4>

    Figure 12-12   Solution for a problem of size 2

    The process of solving problem \\(f(2)\\) can be summarized as: moving two discs from A to C with the help of B. Here, C is called the target pillar, and B is called the buffer pillar.

    ","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2-subproblem-decomposition","level":3,"title":"2.   Subproblem Decomposition","text":"

    For problem \\(f(3)\\), when there are three discs, the situation becomes slightly more complex.

    Since we already know the solutions to \\(f(1)\\) and \\(f(2)\\), we can think from a divide and conquer perspective, treating the top two discs on A as a whole, and execute the steps shown in Figure 12-13. This successfully moves the three discs from A to C.

    1. Let B be the target pillar and C be the buffer pillar, and move two discs from A to B.
    2. Move the remaining disc from A directly to C.
    3. Let C be the target pillar and A be the buffer pillar, and move two discs from B to C.
    <1><2><3><4>

    Figure 12-13   Solution for a problem of size 3

    Essentially, we divide problem \\(f(3)\\) into two subproblems \\(f(2)\\) and one subproblem \\(f(1)\\). By solving these three subproblems in order, the original problem is solved. This shows that the subproblems are independent and their solutions can be merged.

    From this, we can summarize the divide and conquer strategy for solving the hanota problem shown in Figure 12-14: divide the original problem \\(f(n)\\) into two subproblems \\(f(n-1)\\) and one subproblem \\(f(1)\\), and solve these three subproblems in the following order.

    1. Move \\(n-1\\) discs from A to B with the help of C.
    2. Move the remaining \\(1\\) disc directly from A to C.
    3. Move \\(n-1\\) discs from B to C with the help of A.

    For these two subproblems \\(f(n-1)\\), we can recursively divide them in the same way until reaching the smallest subproblem \\(f(1)\\). The solution to \\(f(1)\\) is known and requires only one move operation.

    Figure 12-14   Divide and conquer strategy for solving the hanota problem

    ","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

    In the code, we declare a recursive function dfs(i, src, buf, tar), whose purpose is to move the top \\(i\\) discs from pillar src to target pillar tar with the help of buffer pillar buf:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
    def move(src: list[int], tar: list[int]):\n    \"\"\"Move a disk\"\"\"\n    # Take out a disk from the top of src\n    pan = src.pop()\n    # Place the disk on top of tar\n    tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n    \"\"\"Solve the Tower of Hanoi problem f(i)\"\"\"\n    # If there is only one disk left in src, move it directly to tar\n    if i == 1:\n        move(src, tar)\n        return\n    # Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf)\n    # Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    # Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n    \"\"\"Solve the Tower of Hanoi problem\"\"\"\n    n = len(A)\n    # Move the top n disks from A to C using B\n    dfs(n, A, B, C)\n
    hanota.cpp
    /* Move a disk */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // Take out a disk from the top of src\n    int pan = src.back();\n    src.pop_back();\n    // Place the disk on top of tar\n    tar.push_back(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n    int n = A.size();\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.java
    /* Move a disk */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // Take out a disk from the top of src\n    Integer pan = src.remove(src.size() - 1);\n    // Place the disk on top of tar\n    tar.add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n    int n = A.size();\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.cs
    /* Move a disk */\nvoid Move(List<int> src, List<int> tar) {\n    // Take out a disk from the top of src\n    int pan = src[^1];\n    src.RemoveAt(src.Count - 1);\n    // Place the disk on top of tar\n    tar.Add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid DFS(int i, List<int> src, List<int> buf, List<int> tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    DFS(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    Move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    DFS(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid SolveHanota(List<int> A, List<int> B, List<int> C) {\n    int n = A.Count;\n    // Move the top n disks from A to C using B\n    DFS(n, A, B, C);\n}\n
    hanota.go
    /* Move a disk */\nfunc move(src, tar *list.List) {\n    // Take out a disk from the top of src\n    pan := src.Back()\n    // Place the disk on top of tar\n    tar.PushBack(pan.Value)\n    // Remove top disk from src\n    src.Remove(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfsHanota(i-1, src, tar, buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // Move the top n disks from A to C using B\n    dfsHanota(n, A, B, C)\n}\n
    hanota.swift
    /* Move a disk */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // Take out a disk from the top of src\n    let pan = src.popLast()!\n    // Place the disk on top of tar\n    tar.append(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src: &src, tar: &tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n    let n = A.count\n    // The tail of the list is the top of the rod\n    // Move top n disks from src to C using B\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
    hanota.js
    /* Move a disk */\nfunction move(src, tar) {\n    // Take out a disk from the top of src\n    const pan = src.pop();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunction dfs(i, src, buf, tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.ts
    /* Move a disk */\nfunction move(src: number[], tar: number[]): void {\n    // Take out a disk from the top of src\n    const pan = src.pop();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n    // If there is only one disk left in src, move it directly to tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n    const n = A.length;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.dart
    /* Move a disk */\nvoid move(List<int> src, List<int> tar) {\n  // Take out a disk from the top of src\n  int pan = src.removeLast();\n  // Place the disk on top of tar\n  tar.add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, List<int> src, List<int> buf, List<int> tar) {\n  // If there is only one disk left in src, move it directly to tar\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n  dfs(i - 1, src, tar, buf);\n  // Subproblem f(1): move the remaining disk from src to tar\n  move(src, tar);\n  // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n  dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(List<int> A, List<int> B, List<int> C) {\n  int n = A.length;\n  // Move the top n disks from A to C using B\n  dfs(n, A, B, C);\n}\n
    hanota.rs
    /* Move a disk */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // Take out a disk from the top of src\n    let pan = src.pop().unwrap();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move_pan(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {\n    let n = A.len() as i32;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
    hanota.c
    /* Move a disk */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // Take out a disk from the top of src\n    int pan = src[*srcSize - 1];\n    src[*srcSize - 1] = 0;\n    (*srcSize)--;\n    // Place the disk on top of tar\n    tar[*tarSize] = pan;\n    (*tarSize)++;\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, srcSize, tar, tarSize);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n    // Move the top n disks from A to C using B\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
    hanota.kt
    /* Move a disk */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // Take out a disk from the top of src\n    val pan = src.removeAt(src.size - 1)\n    // Place the disk on top of tar\n    tar.add(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {\n    val n = A.size\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C)\n}\n
    hanota.rb
    ### Move one disk ###\ndef move(src, tar)\n  # Take out a disk from the top of src\n  pan = src.pop\n  # Place the disk on top of tar\n  tar << pan\nend\n\n### Solve Tower of Hanoi f(i) ###\ndef dfs(i, src, buf, tar)\n  # If there is only one disk left in src, move it directly to tar\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n  dfs(i - 1, src, tar, buf)\n  # Subproblem f(1): move the remaining disk from src to tar\n  move(src, tar)\n  # Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n  dfs(i - 1, buf, src, tar)\nend\n\n### Solve Tower of Hanoi ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # Move the top n disks from A to C using B\n  dfs(n, _A, _B, _C)\nend\n

    As shown in Figure 12-15, the hanota problem forms a recursion tree of height \\(n\\), where each node represents a subproblem corresponding to an invocation of the dfs() function, therefore the time complexity is \\(O(2^n)\\) and the space complexity is \\(O(n)\\).

    Figure 12-15   Recursion tree of the hanota problem

    Quote

    The hanota problem originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and \\(64\\) golden discs of different sizes. The monks continuously moved the discs, believing that when the last disc was correctly placed, the world would come to an end.

    However, even if the monks moved one disc per second, it would take approximately \\(2^{64} \\approx 1.84×10^{19}\\) seconds, which is about \\(585\\) billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world.

    ","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   Summary","text":"","path":["Chapter 12. Divide and Conquer","12.5   Summary"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Divide and conquer is a common algorithm design strategy consisting of two phases, divide (partition) and conquer (merge), and is typically implemented recursively.
    • The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged.
    • Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting.
    • Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, it reduces the number of operations; on the other hand, it makes parallel optimization by the system easier.
    • Divide and conquer can solve many algorithmic problems and is also widely used in data structures and algorithm design, making it ubiquitous.
    • Compared to brute-force search, adaptive search is more efficient. Search algorithms with time complexity of \\(O(\\log n)\\) are typically implemented based on the divide and conquer strategy.
    • Binary search is another typical application of divide and conquer. It does not include the step of merging solutions of subproblems. We can implement binary search through recursive divide and conquer.
    • In the problem of building a binary tree, building the tree (original problem) can be divided into building the left subtree and right subtree (subproblems), which can be achieved by dividing the index intervals of the preorder and inorder traversals.
    • In the hanota problem, a problem of size \\(n\\) can be divided into two subproblems of size \\(n-1\\) and one subproblem of size \\(1\\). After solving these three subproblems in order, the original problem is solved.
    ","path":["Chapter 12. Divide and Conquer","12.5   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"Chapter 14.   Dynamic Programming","text":"

    Abstract

    Streams flow into rivers, rivers flow into the sea.

    Dynamic programming combines solutions to small problems into the answer to a large problem, leading us step by step to the other shore of problem-solving.

    ","path":["Chapter 14. Dynamic Programming","Chapter 14.   Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 14.1   Introduction to Dynamic Programming
    • 14.2   Characteristics of Dynamic Programming Problems
    • 14.3   Dynamic Programming Problem-Solving Approach
    • 14.4   0-1 Knapsack Problem
    • 14.5   Unbounded Knapsack Problem
    • 14.6   Edit Distance Problem
    • 14.7   Summary
    ","path":["Chapter 14. Dynamic Programming","Chapter 14.   Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   Characteristics of Dynamic Programming Problems","text":"

    In the previous section, we learned how dynamic programming solves the original problem by decomposing it into subproblems. In fact, subproblem decomposition is a general algorithmic approach, with different emphases in divide and conquer, dynamic programming, and backtracking.

    • Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and merge the solutions to the subproblems during backtracking to ultimately obtain the solution to the original problem.
    • Dynamic programming also recursively decomposes problems, but the main difference from divide and conquer algorithms is that subproblems in dynamic programming are interdependent, and many overlapping subproblems appear during the decomposition process.
    • Backtracking algorithms enumerate all possible solutions through trial and error, and avoid unnecessary search branches through pruning. The solution to the original problem consists of a series of decision steps, and we can regard the subsequence before each decision step as a subproblem.

    In fact, dynamic programming is commonly used to solve optimization problems, which not only contain overlapping subproblems but also have two other major characteristics: optimal substructure and no aftereffects.

    ","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421-optimal-substructure","level":2,"title":"14.2.1   Optimal Substructure","text":"

    We make a slight modification to the stair climbing problem to make it more suitable for demonstrating the concept of optimal substructure.

    Climbing stairs with minimum cost

    Given a staircase, you can climb \\(1\\) or \\(2\\) steps at a time, and each step is labeled with a non-negative integer representing the cost of stepping on it. Given a non-negative integer array \\(cost\\), where \\(cost[i]\\) represents the cost of the \\(i\\)-th step and \\(cost[0]\\) is the ground (starting point), what is the minimum cost required to reach the top?

    As shown in Figure 14-6, if the costs of the \\(1\\)st, \\(2\\)nd, and \\(3\\)rd steps are \\(1\\), \\(10\\), and \\(1\\) respectively, then climbing from the ground to the \\(3\\)rd step requires a minimum cost of \\(2\\).

    Figure 14-6   Minimum cost to climb to the 3rd step

    Let \\(dp[i]\\) be the accumulated cost of climbing to the \\(i\\)-th step. Since the \\(i\\)-th step can only come from the \\(i-1\\)-th or \\(i-2\\)-th step, \\(dp[i]\\) can only equal \\(dp[i-1] + cost[i]\\) or \\(dp[i-2] + cost[i]\\). To minimize the cost, we should choose the smaller of the two:

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

    This leads us to the meaning of optimal substructure: the optimal solution to the original problem is constructed from the optimal solutions to the subproblems.

    This problem clearly has optimal substructure: we select the better one from the optimal solutions to the two subproblems \\(dp[i-1]\\) and \\(dp[i-2]\\), and use it to construct the optimal solution to the original problem \\(dp[i]\\).

    So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to find the number of ways, which seems to be a counting problem, but if we change the question: \"Find the maximum number of ways\". We surprisingly discover that although the problem before and after modification are equivalent, the optimal substructure has emerged: the maximum number of ways for the \\(n\\)-th step equals the sum of the maximum number of ways for the \\(n-1\\)-th and \\(n-2\\)-th steps. Therefore, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems.

    According to the state transition equation and the initial states \\(dp[1] = cost[1]\\) and \\(dp[2] = cost[2]\\), we can obtain the dynamic programming code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
    def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n    \"\"\"Minimum cost climbing stairs: Dynamic programming\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [0] * (n + 1)\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1], dp[2] = cost[1], cost[2]\n    # State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    vector<int> dp(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint MinCostClimbingStairsDP(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\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    // Initialize dp table, used to store solutions to subproblems\n    dp := make([]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nfunc minCostClimbingStairsDP(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: 0, count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nfunction minCostClimbingStairsDP(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nfunction minCostClimbingStairsDP(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  // Initialize dp table, used to store solutions to subproblems\n  List<int> dp = List.filled(n + 1, 0);\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1] = cost[1];\n  dp[2] = cost[2];\n  // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\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    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![-1; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int *dp = calloc(n + 1, sizeof(int));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\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    // Free memory\n    free(dp);\n    return res;\n}\n
    min_cost_climbing_stairs_dp.kt
    /* Minimum cost climbing stairs: Dynamic programming */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = IntArray(n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\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
    ### Minimum cost climbing stairs: DP ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = cost[1], cost[2]\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n

    Figure 14-7 shows the dynamic programming process for the above code.

    Figure 14-7   Dynamic programming process for climbing stairs with minimum cost

    This problem can also be space-optimized, compressing from one dimension to zero, reducing the space complexity from \\(O(n)\\) to \\(O(1)\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
    def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n    \"\"\"Minimum cost climbing stairs: Space-optimized dynamic programming\"\"\"\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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    // Initial state: preset the solution to the smallest subproblem\n    a, b := cost[1], cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    /* Minimum cost climbing stairs: Space-optimized dynamic programming */\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
    ### Minimum cost climbing stairs: DP ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = cost[1], cost[2]\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n\n# Minimum cost climbing stairs: Space-optimized dynamic programming\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":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422-no-aftereffects","level":2,"title":"14.2.2   No Aftereffects","text":"

    No aftereffects is one of the important characteristics that enable dynamic programming to solve problems effectively. Its definition is: given a certain state, its future development is only related to the current state and has nothing to do with all past states.

    Taking the stair climbing problem as an example, given state \\(i\\), it will develop into states \\(i+1\\) and \\(i+2\\), corresponding to jumping \\(1\\) step and jumping \\(2\\) steps, respectively. When making these two choices, we do not need to consider the states before state \\(i\\), as they have no effect on the future of state \\(i\\).

    However, if we add a constraint to the stair climbing problem, the situation changes.

    Climbing stairs with constraint

    Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time, but you cannot jump \\(1\\) step in two consecutive rounds. How many ways are there to climb to the top?

    As shown in Figure 14-8, there are only \\(2\\) feasible ways to climb to the \\(3\\)rd step. The path with three consecutive \\(1\\)-step jumps does not satisfy the constraint and is therefore discarded.

    Figure 14-8   Number of ways to climb to the 3rd step with constraint

    In this problem, if the previous round was a jump of \\(1\\) step, then the next round must jump \\(2\\) steps. This means that the next choice cannot be determined solely by the current state (current stair step number), but also depends on the previous state (the stair step number from the previous round).

    It is not difficult to see that this problem no longer satisfies no aftereffects, and the state transition equation \\(dp[i] = dp[i-1] + dp[i-2]\\) also fails, because \\(dp[i-1]\\) represents jumping \\(1\\) step in this round, but it includes many solutions where \"the previous round was a jump of \\(1\\) step\", which cannot be directly counted in \\(dp[i]\\) to satisfy the constraint.

    For this reason, we need to expand the state definition: state \\([i, j]\\) represents being on the \\(i\\)-th step with the previous round having jumped \\(j\\) steps, where \\(j \\in \\{1, 2\\}\\). This state definition effectively distinguishes whether the previous round was a jump of \\(1\\) step or \\(2\\) steps, allowing us to determine where the current state came from.

    • When the previous round jumped \\(1\\) step, the round before that could only choose to jump \\(2\\) steps, i.e., \\(dp[i, 1]\\) can only transition from \\(dp[i-1, 2]\\).
    • When the previous round jumped \\(2\\) steps, the round before that could choose to jump \\(1\\) step or \\(2\\) steps, i.e., \\(dp[i, 2]\\) can transition from \\(dp[i-2, 1]\\) or \\(dp[i-2, 2]\\).

    As shown in Figure 14-9, under this definition, \\(dp[i, j]\\) represents the number of ways for state \\([i, j]\\). The state transition equation is then:

    \\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\]

    Figure 14-9   Recurrence relation considering constraints

    Finally, return \\(dp[n, 1] + dp[n, 2]\\), where the sum of the two represents the total number of ways to climb to the \\(n\\)-th step:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py
    def climbing_stairs_constraint_dp(n: int) -> int:\n    \"\"\"Climbing stairs with constraint: Dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return 1\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [[0] * 3 for _ in range(n + 1)]\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1][1], dp[1][2] = 1, 0\n    dp[2][1], dp[2][2] = 0, 1\n    # State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int[][] dp = new int[n + 1][3];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint ClimbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int[,] dp = new int[n + 1, 3];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1, 1] = 1;\n    dp[1, 2] = 0;\n    dp[2, 1] = 0;\n    dp[2, 2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfunc climbingStairsConstraintDP(n int) int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([][3]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfunc climbingStairsConstraintDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfunction climbingStairsConstraintDP(n) {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = Array.from(new Array(n + 1), () => new Array(3));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfunction climbingStairsConstraintDP(n: number): number {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = Array.from({ length: n + 1 }, () => new Array(3));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n  if (n == 1 || n == 2) {\n    return 1;\n  }\n  // Initialize dp table, used to store solutions to subproblems\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1][1] = 1;\n  dp[1][2] = 0;\n  dp[2][1] = 0;\n  dp[2][2] = 1;\n  // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nfn climbing_stairs_constraint_dp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return 1;\n    };\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![vec![-1; 3]; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\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    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\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    // Free memory\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
    /* Climbing stairs with constraint: Dynamic programming */\nfun climbingStairsConstraintDP(n: Int): Int {\n    if (n == 1 || n == 2) {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = Array(n + 1) { IntArray(3) }\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\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
    ### Climbing stairs with constraint: DP ###\ndef climbing_stairs_constraint_dp(n)\n  return 1 if n == 1 || n == 2\n\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1) { Array.new(3, 0) }\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1][1], dp[1][2] = 1, 0\n  dp[2][1], dp[2][2] = 0, 1\n  # State transition: gradually solve larger subproblems from smaller ones\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

    In the above case, since we only need to consider one more preceding state, we can still make the problem satisfy no aftereffects by expanding the state definition. However, some problems have very severe \"aftereffects\".

    Climbing stairs with obstacle generation

    Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time. Whenever you reach the \\(i\\)-th step, the system automatically places an obstacle on the \\(2i\\)-th step, and no subsequent round is allowed to jump to the \\(2i\\)-th step. For example, if the first two rounds jump to the \\(2\\)nd and \\(3\\)rd steps, then afterwards you cannot jump to the \\(4\\)th and \\(6\\)th steps. How many ways are there to climb to the top?

    In this problem, the next jump depends on all past states, because each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming is often difficult to solve.

    In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually use other methods, such as heuristic search, genetic algorithms, and reinforcement learning, to obtain usable locally optimal solutions within a limited time.

    ","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   Dynamic Programming Problem-Solving Approach","text":"

    The previous two sections introduced the main characteristics of dynamic programming problems. Next, let us explore two more practical issues together.

    1. How to determine whether a problem is a dynamic programming problem?
    2. What is the complete process for solving a dynamic programming problem, and where should we start?
    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431-problem-identification","level":2,"title":"14.3.1   Problem Identification","text":"

    Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and satisfies no aftereffects, then it is usually suitable for solving with dynamic programming. However, it is difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and first observe whether the problem is suitable for solving with backtracking (exhaustive search).

    Problems suitable for solving with backtracking usually satisfy the \"decision tree model\", which means the problem can be described using a tree structure, where each node represents a decision and each path represents a sequence of decisions.

    In other words, if a problem contains an explicit concept of decisions, and the solution is generated through a series of decisions, then it satisfies the decision tree model and can usually be solved using backtracking.

    On this basis, dynamic programming problems also have some positive indicators.

    • The problem contains descriptions such as maximum (minimum) or most (least), indicating optimization.
    • The problem's state can be represented using a list, multi-dimensional matrix, or tree, and a state has a recurrence relation with its surrounding states.

    Correspondingly, there are also some negative indicators.

    • The goal of the problem is to find all possible solutions, rather than finding the optimal solution.
    • The problem description has obvious permutation and combination characteristics, requiring the return of specific multiple solutions.

    If a problem satisfies the decision tree model and has relatively obvious positive indicators, we can assume it is a dynamic programming problem and verify that assumption during the solving process.

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432-problem-solving-steps","level":2,"title":"14.3.2   Problem-Solving Steps","text":"

    The problem-solving process for dynamic programming varies depending on the nature and difficulty of the problem, but generally follows these steps: describe decisions, define states, establish the \\(dp\\) table, derive state transition equations, determine boundary conditions, etc.

    To illustrate the problem-solving steps more vividly, we use a classic problem \"minimum path sum\" as an example.

    Question

    Given an \\(n \\times m\\) two-dimensional grid grid in which each cell contains a non-negative integer representing its cost, a robot starts from the top-left cell and can only move down or right at each step until reaching the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right.

    Figure 14-10 shows an example where the minimum path sum for the given grid is \\(13\\).

    Figure 14-10   Minimum path sum example data

    Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

    The decision in each round of this problem is to move one step down or right from the current cell. Let the row and column indices of the current cell be \\([i, j]\\). After moving down or right, the indices become \\([i+1, j]\\) or \\([i, j+1]\\). Therefore, the state should include two variables, the row index and column index, denoted as \\([i, j]\\).

    State \\([i, j]\\) corresponds to the subproblem: the minimum path sum from the starting point \\([0, 0]\\) to \\([i, j]\\), denoted as \\(dp[i, j]\\).

    From this, we obtain the two-dimensional \\(dp\\) matrix shown in Figure 14-11, whose size is the same as the input grid \\(grid\\).

    Figure 14-11   State definition and dp table

    Note

    The dynamic programming and backtracking processes can be described as a sequence of decisions, and the state consists of all decision variables. It should contain all variables describing the progress of problem-solving, and should contain sufficient information to derive the next state.

    Each state corresponds to a subproblem, and we define a \\(dp\\) table to store the solutions to all subproblems. Each independent variable of the state is a dimension of the \\(dp\\) table. Essentially, the \\(dp\\) table is a mapping between states and solutions to subproblems.

    Step 2: Identify the optimal substructure, and then derive the state transition equation

    For state \\([i, j]\\), it can only transition from the cell above \\([i-1, j]\\) or the cell to the left \\([i, j-1]\\). Therefore, the optimal substructure is: the minimum path sum to reach \\([i, j]\\) is determined by the smaller of the minimum path sums of \\([i, j-1]\\) and \\([i-1, j]\\).

    Based on the above analysis, the state transition equation shown in Figure 14-12 can be derived:

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

    Figure 14-12   Optimal substructure and state transition equation

    Note

    Based on the defined \\(dp\\) table, think about the relationship between the original problem and subproblems, and find the method to construct the optimal solution to the original problem from the optimal solutions to the subproblems, which is the optimal substructure.

    Once we identify the optimal substructure, we can use it to construct the state transition equation.

    Step 3: Determine boundary conditions and state transition order

    In this problem, states in the first row can only come from the state to their left, and states in the first column can only come from the state above them. Therefore, the first row \\(i = 0\\) and first column \\(j = 0\\) are boundary conditions.

    As shown in Figure 14-13, since each cell transitions from the cell to its left and the cell above it, we use loops to traverse the matrix, with the outer loop traversing rows and the inner loop traversing columns.

    Figure 14-13   Boundary conditions and state transition order

    Note

    Boundary conditions in dynamic programming are used to initialize the \\(dp\\) table, while in search they are used for pruning.

    The core of state transition order is to ensure that when computing the solution to the current problem, all the smaller subproblems it depends on have already been computed correctly.

    Based on the above analysis, we can directly write the dynamic programming code. However, subproblem decomposition is a top-down approach, so implementing in the order \"brute force search \\(\\rightarrow\\) memoization \\(\\rightarrow\\) dynamic programming\" is more aligned with thinking habits.

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-method-1-brute-force-search","level":3,"title":"1.   Method 1: Brute Force Search","text":"

    Starting from state \\([i, j]\\), we continuously decompose it into smaller states \\([i-1, j]\\) and \\([i, j-1]\\). The recursive function includes the following elements.

    • Recursive parameters: state \\([i, j]\\).
    • Return value: minimum path sum from \\([0, 0]\\) to \\([i, j]\\), which is \\(dp[i, j]\\).
    • Termination condition: when \\(i = 0\\) and \\(j = 0\\), return cost \\(grid[0, 0]\\).
    • Pruning: when \\(i < 0\\) or \\(j < 0\\), the index is out of bounds, return cost \\(+\\infty\\), representing infeasibility.

    The implementation code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
    def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n    \"\"\"Minimum path sum: Brute-force search\"\"\"\n    # If it's the top-left cell, terminate the search\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # If row or column index is out of bounds, return +∞ cost\n    if i < 0 or j < 0:\n        return inf\n    # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    up = min_path_sum_dfs(grid, i - 1, j)\n    left = min_path_sum_dfs(grid, i, j - 1)\n    # Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n
    min_path_sum.cpp
    /* Minimum path sum: Brute-force search */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n
    min_path_sum.java
    /* Minimum path sum: Brute-force search */\nint minPathSumDFS(int[][] grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
    min_path_sum.cs
    /* Minimum path sum: Brute-force search */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = MinPathSumDFS(grid, i - 1, j);\n    int left = MinPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.Min(left, up) + grid[i][j];\n}\n
    min_path_sum.go
    /* Minimum path sum: Brute-force search */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    up := minPathSumDFS(grid, i-1, j)\n    left := minPathSumDFS(grid, i, j-1)\n    // Return the minimum path cost from top-left to (i, j)\n    return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n
    min_path_sum.swift
    /* Minimum path sum: Brute-force search */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {\n    // If it's the top-left cell, terminate the search\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (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    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n}\n
    min_path_sum.js
    /* Minimum path sum: Brute-force search */\nfunction minPathSumDFS(grid, i, j) {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
    min_path_sum.ts
    /* Minimum path sum: Brute-force search */\nfunction minPathSumDFS(\n    grid: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
    min_path_sum.dart
    /* Minimum path sum: Brute-force search */\nint minPathSumDFS(List<List<int>> grid, int i, int j) {\n  // If it's the top-left cell, terminate the search\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // If row or column index is out of bounds, return +∞ cost\n  if (i < 0 || j < 0) {\n    // In Dart, int type is fixed-range integer, no value representing \"infinity\"\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n  int up = minPathSumDFS(grid, i - 1, j);\n  int left = minPathSumDFS(grid, i, j - 1);\n  // Return the minimum path cost from top-left to (i, j)\n  return min(left, up) + grid[i][j];\n}\n
    min_path_sum.rs
    /* Minimum path sum: Brute-force search */\nfn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (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    // Return the minimum path cost from top-left to (i, j)\n    std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n
    min_path_sum.c
    /* Minimum path sum: Brute-force search */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n
    min_path_sum.kt
    /* Minimum path sum: Brute-force search */\nfun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    val up = minPathSumDFS(grid, i - 1, j)\n    val left = minPathSumDFS(grid, i, j - 1)\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n}\n
    min_path_sum.rb
    ### Minimum path sum: brute force search ###\ndef min_path_sum_dfs(grid, i, j)\n  # If it's the top-left cell, terminate the search\n  return grid[i][j] if i == 0 && j == 0\n  # If row or column index is out of bounds, return +∞ cost\n  return Float::INFINITY if i < 0 || j < 0\n  # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n  up = min_path_sum_dfs(grid, i - 1, j)\n  left = min_path_sum_dfs(grid, i, j - 1)\n  # Return the minimum path cost from top-left to (i, j)\n  [left, up].min + grid[i][j]\nend\n

    Figure 14-14 shows the recursion tree rooted at \\(dp[2, 1]\\), which includes some overlapping subproblems whose number will increase sharply as the size of grid grid grows.

    Essentially, the reason for overlapping subproblems is: there are multiple paths from the top-left corner to reach a certain cell.

    Figure 14-14   Brute force search recursion tree

    Each state has two choices, down and right, so the total number of steps from the top-left corner to the bottom-right corner is \\(m + n - 2\\), giving a worst-case time complexity of \\(O(2^{m + n})\\), where \\(n\\) and \\(m\\) are the number of rows and columns of the grid, respectively. Note that this calculation does not account for situations near the grid boundaries, where only one choice remains when reaching the grid boundary, so the actual number of paths will be somewhat less.

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-method-2-memoization","level":3,"title":"2.   Method 2: Memoization","text":"

    We introduce a memo list mem of the same size as grid grid to record the solutions to subproblems and prune overlapping subproblems:

    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    \"\"\"Minimum path sum: Memoization search\"\"\"\n    # If it's the top-left cell, terminate the search\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # If row or column index is out of bounds, return +∞ cost\n    if i < 0 or j < 0:\n        return inf\n    # If there's a record, return it directly\n    if mem[i][j] != -1:\n        return mem[i][j]\n    # Minimum path cost for left and upper cells\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    # Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n
    min_path_sum.cpp
    /* Minimum path sum: Memoization search */\nint minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (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
    /* Minimum path sum: Memoization search */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
    min_path_sum.cs
    /* Minimum path sum: Memoization search */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n    int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.Min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
    min_path_sum.go
    /* Minimum path sum: Memoization search */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // If there's a record, return it directly\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    up := minPathSumDFSMem(grid, mem, i-1, j)\n    left := minPathSumDFSMem(grid, mem, i, j-1)\n    // Record and return the minimum path cost from top-left to (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
    /* Minimum path sum: Memoization search */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {\n    // If it's the top-left cell, terminate the search\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // If there's a record, return it directly\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\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    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
    min_path_sum.js
    /* Minimum path sum: Memoization search */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] !== -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
    min_path_sum.ts
    /* Minimum path sum: Memoization search */\nfunction minPathSumDFSMem(\n    grid: Array<Array<number>>,\n    mem: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
    min_path_sum.dart
    /* Minimum path sum: Memoization search */\nint minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {\n  // If it's the top-left cell, terminate the search\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // If row or column index is out of bounds, return +∞ cost\n  if (i < 0 || j < 0) {\n    // In Dart, int type is fixed-range integer, no value representing \"infinity\"\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // If there's a record, return it directly\n  if (mem[i][j] != -1) {\n    return mem[i][j];\n  }\n  // Minimum path cost for left and upper cells\n  int up = minPathSumDFSMem(grid, mem, i - 1, j);\n  int left = minPathSumDFSMem(grid, mem, i, j - 1);\n  // Record and return the minimum path cost from top-left to (i, j)\n  mem[i][j] = min(left, up) + grid[i][j];\n  return mem[i][j];\n}\n
    min_path_sum.rs
    /* Minimum path sum: Memoization search */\nfn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // If there's a record, return it directly\n    if mem[i as usize][j as usize] != -1 {\n        return mem[i as usize][j as usize];\n    }\n    // Minimum path cost for left and upper cells\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    // Record and return the minimum path cost from top-left to (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
    /* Minimum path sum: Memoization search */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (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
    /* Minimum path sum: Memoization search */\nfun minPathSumDFSMem(\n    grid: Array<IntArray>,\n    mem: Array<IntArray>,\n    i: Int,\n    j: Int\n): Int {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    val up = minPathSumDFSMem(grid, mem, i - 1, j)\n    val left = minPathSumDFSMem(grid, mem, i, j - 1)\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
    min_path_sum.rb
    ### Minimum path sum: memoization search ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n  # If it's the top-left cell, terminate the search\n  return grid[0][0] if i == 0 && j == 0\n  # If row or column index is out of bounds, return +∞ cost\n  return Float::INFINITY if i < 0 || j < 0\n  # If there's a record, return it directly\n  return mem[i][j] if mem[i][j] != -1\n  # Minimum path cost for left and upper cells\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  # Record and return the minimum path cost from top-left to (i, j)\n  mem[i][j] = [left, up].min + grid[i][j]\nend\n

    As shown in Figure 14-15, after introducing memoization, all subproblem solutions only need to be computed once, so the time complexity depends on the total number of states, which is the grid size \\(O(nm)\\).

    Figure 14-15   Memoization recursion tree

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-method-3-dynamic-programming","level":3,"title":"3.   Method 3: Dynamic Programming","text":"

    Implement the dynamic programming solution based on iteration, as shown in the code below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
    def min_path_sum_dp(grid: list[list[int]]) -> int:\n    \"\"\"Minimum path sum: Dynamic programming\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Initialize dp table\n    dp = [[0] * m for _ in range(n)]\n    dp[0][0] = grid[0][0]\n    # State transition: first row\n    for j in range(1, m):\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    # State transition: first column\n    for i in range(1, n):\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    # State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint minPathSumDP(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Initialize dp table\n    vector<vector<int>> dp(n, vector<int>(m));\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint minPathSumDP(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Initialize dp table\n    int[][] dp = new int[n][m];\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint MinPathSumDP(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Initialize dp table\n    int[,] dp = new int[n, m];\n    dp[0, 0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0, j] = dp[0, j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfunc minPathSumDP(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Initialize dp table\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    // State transition: first row\n    for j := 1; j < m; j++ {\n        dp[0][j] = dp[0][j-1] + grid[0][j]\n    }\n    // State transition: first column\n    for i := 1; i < n; i++ {\n        dp[i][0] = dp[i-1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfunc minPathSumDP(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for j in 1 ..< m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // State transition: first column\n    for i in 1 ..< n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfunction minPathSumDP(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfunction minPathSumDP(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint minPathSumDP(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));\n  dp[0][0] = grid[0][0];\n  // State transition: first row\n  for (int j = 1; j < m; j++) {\n    dp[0][j] = dp[0][j - 1] + grid[0][j];\n  }\n  // State transition: first column\n  for (int i = 1; i < n; i++) {\n    dp[i][0] = dp[i - 1][0] + grid[i][0];\n  }\n  // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nfn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Initialize dp table\n    let mut dp = vec![vec![0; m]; n];\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for j in 1..m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for i in 1..n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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
    /* Minimum path sum: Dynamic programming */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Initialize dp table\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    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\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    // Free memory\n    for (int i = 0; i < n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
    min_path_sum.kt
    /* Minimum path sum: Dynamic programming */\nfun minPathSumDP(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Initialize dp table\n    val dp = Array(n) { IntArray(m) }\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for (j in 1..<m) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // State transition: first column\n    for (i in 1..<n) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\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
    ### Minimum path sum: dynamic programming ###\ndef min_path_sum_dp(grid)\n  n, m = grid.length, grid.first.length\n  # Initialize dp table\n  dp = Array.new(n) { Array.new(m, 0) }\n  dp[0][0] = grid[0][0]\n  # State transition: first row\n  (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n  # State transition: first column\n  (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n  # State transition: rest of the rows and columns\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

    Figure 14-16 shows the state transition process for minimum path sum, which traverses the entire grid, thus the time complexity is \\(O(nm)\\).

    The array dp has size \\(n \\times m\\), thus the space complexity is \\(O(nm)\\).

    <1><2><3><4><5><6><7><8><9><10><11><12>

    Figure 14-16   Dynamic programming process for minimum path sum

    ","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4-space-optimization","level":3,"title":"4.   Space Optimization","text":"

    Since each cell is only related to the cell to its left and the cell above it, we can use a single-row array to implement the \\(dp\\) table.

    Note that since the array dp can only represent the state of one row, we cannot initialize the first column state in advance, but rather update it when traversing each row:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
    def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n    \"\"\"Minimum path sum: Space-optimized dynamic programming\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Initialize dp table\n    dp = [0] * m\n    # State transition: first row\n    dp[0] = grid[0][0]\n    for j in range(1, m):\n        dp[j] = dp[j - 1] + grid[0][j]\n    # State transition: rest of the rows\n    for i in range(1, n):\n        # State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        # State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Initialize dp table\n    vector<int> dp(m);\n    // State transition: first row\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    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Initialize dp table\n    int[] dp = new int[m];\n    // State transition: first row\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    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint MinPathSumDPComp(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Initialize dp table\n    int[] dp = new int[m];\n    dp[0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfunc minPathSumDPComp(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Initialize dp table\n    dp := make([]int, m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for j := 1; j < m; j++ {\n        dp[j] = dp[j-1] + grid[0][j]\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i < n; i++ {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfunc minPathSumDPComp(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for j in 1 ..< m {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // State transition: rest of the rows\n    for i in 1 ..< n {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfunction minPathSumDPComp(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = new Array(m);\n    // State transition: first row\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    // State transition: rest of the rows\n    for (let i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfunction minPathSumDPComp(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = new Array(m);\n    // State transition: first row\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    // State transition: rest of the rows\n    for (let i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Initialize dp table\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  // State transition: rest of the rows\n  for (int i = 1; i < n; i++) {\n    // State transition: first column\n    dp[0] = dp[0] + grid[i][0];\n    // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nfn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Initialize dp table\n    let mut dp = vec![0; m];\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for j in 1..m {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for i in 1..n {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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
    /* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Initialize dp table\n    int *dp = calloc(m, sizeof(int));\n    // State transition: first row\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    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\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    // Free memory\n    free(dp);\n    return res;\n}\n
    min_path_sum.kt
    /* Minimum path sum: Space-optimized dynamic programming */\nfun minPathSumDPComp(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Initialize dp table\n    val dp = IntArray(m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for (j in 1..<m) {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // State transition: rest of the rows\n    for (i in 1..<n) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\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
    ### Minimum path sum: space-optimized DP ###\ndef min_path_sum_dp_comp(grid)\n  n, m = grid.length, grid.first.length\n  # Initialize dp table\n  dp = Array.new(m, 0)\n  # State transition: first row\n  dp[0] = grid[0][0]\n  (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n  # State transition: rest of the rows\n  for i in 1...n\n    # State transition: first column\n    dp[0] = dp[0] + grid[i][0]\n    # State transition: rest of the columns\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":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   Edit Distance Problem","text":"

    Edit distance, also known as Levenshtein distance, refers to the minimum number of edits required to transform one string into another, commonly used in information retrieval and natural language processing to measure the similarity between two sequences.

    Question

    Given two strings \\(s\\) and \\(t\\), return the minimum number of edits required to transform \\(s\\) into \\(t\\).

    You can perform three types of edit operations on a string: insert a character, delete a character, or replace a character with any other character.

    As shown in Figure 14-27, transforming kitten into sitting requires 3 edits, including 2 replacements and 1 insertion; transforming hello into algo requires 3 steps, including 2 replacements and 1 deletion.

    Figure 14-27   Example data for edit distance

    The edit distance problem can be naturally explained using the decision tree model. Strings correspond to tree nodes, and each edit operation corresponds to an edge in the tree.

    As shown in Figure 14-28, without restricting operations, each node can branch into many edges, with each edge corresponding to one operation, meaning there are many possible paths to transform hello into algo.

    From the perspective of the decision tree, the goal of this problem is to find the shortest path between node hello and node algo.

    Figure 14-28   Representing edit distance problem based on decision tree model

    ","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1-dynamic-programming-approach","level":3,"title":"1.   Dynamic Programming Approach","text":"

    Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

    Each round of decision involves performing one edit operation on string \\(s\\).

    We want the problem size to gradually decrease during the editing process so that we can construct subproblems. Let the lengths of strings \\(s\\) and \\(t\\) be \\(n\\) and \\(m\\) respectively. We first consider the tail characters of the two strings, \\(s[n-1]\\) and \\(t[m-1]\\).

    • If \\(s[n-1]\\) and \\(t[m-1]\\) are the same, we can skip them and directly consider \\(s[n-2]\\) and \\(t[m-2]\\).
    • If \\(s[n-1]\\) and \\(t[m-1]\\) are different, we need to perform one edit on \\(s\\) (insert, delete, or replace) to make the tail characters of the two strings the same, allowing us to skip them and consider a smaller-scale problem.

    In other words, each round of decision (edit operation) we make on string \\(s\\) will change the remaining characters to be matched in \\(s\\) and \\(t\\). Therefore, the state is the \\(i\\)-th and \\(j\\)-th characters currently being considered in \\(s\\) and \\(t\\), denoted as \\([i, j]\\).

    State \\([i, j]\\) corresponds to the subproblem: the minimum number of edits required to change the first \\(i\\) characters of \\(s\\) into the first \\(j\\) characters of \\(t\\).

    From this, we obtain a two-dimensional \\(dp\\) table of size \\((i+1) \\times (j+1)\\).

    Step 2: Identify the optimal substructure, and then derive the state transition equation

    Consider subproblem \\(dp[i, j]\\), where the tail characters of the corresponding two strings are \\(s[i-1]\\) and \\(t[j-1]\\), which can be divided into the three cases shown in Figure 14-29 based on different edit operations.

    1. Insert \\(t[j-1]\\) after \\(s[i-1]\\), then the remaining subproblem is \\(dp[i, j-1]\\).
    2. Delete \\(s[i-1]\\), then the remaining subproblem is \\(dp[i-1, j]\\).
    3. Replace \\(s[i-1]\\) with \\(t[j-1]\\), then the remaining subproblem is \\(dp[i-1, j-1]\\).

    Figure 14-29   State transition for edit distance

    Based on the above analysis, we obtain the optimal substructure: the minimum number of edits for \\(dp[i, j]\\) equals the minimum of \\(dp[i, j-1]\\), \\(dp[i-1, j]\\), and \\(dp[i-1, j-1]\\), plus the current edit cost of \\(1\\). The corresponding state transition equation is:

    \\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\]

    Please note that when \\(s[i-1]\\) and \\(t[j-1]\\) are the same, no edit is required for the current character, in which case the state transition equation is:

    \\[ dp[i, j] = dp[i-1, j-1] \\]

    Step 3: Determine boundary conditions and state transition order

    When both strings are empty, the number of edit steps is \\(0\\), i.e., \\(dp[0, 0] = 0\\). When \\(s\\) is empty but \\(t\\) is not, the minimum number of edit steps equals the length of \\(t\\), i.e., the first row \\(dp[0, j] = j\\). When \\(s\\) is not empty but \\(t\\) is empty, the minimum number of edit steps equals the length of \\(s\\), i.e., the first column \\(dp[i, 0] = i\\).

    Observing the state transition equation, the solution \\(dp[i, j]\\) depends on solutions to the left, above, and upper-left, so the entire \\(dp\\) table can be traversed in order through two nested loops.

    ","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
    def edit_distance_dp(s: str, t: str) -> int:\n    \"\"\"Edit distance: Dynamic programming\"\"\"\n    n, m = len(s), len(t)\n    dp = [[0] * (m + 1) for _ in range(n + 1)]\n    # State transition: first row and first column\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    # State transition: rest of the rows and columns\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                # If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\nint editDistanceDP(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[][] dp = new int[n + 1][m + 1];\n    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\nint EditDistanceDP(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[,] dp = new int[n + 1, m + 1];\n    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= m; j++ {\n            if s[i-1] == t[j-1] {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\n    for i in 1 ... n {\n        for j in 1 ... m {\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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  // State transition: first row and first column\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  // State transition: rest of the rows and columns\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        // If two characters are equal, skip both characters\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\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                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
    edit_distance.kt
    /* Edit distance: Dynamic programming */\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    // State transition: first row and first column\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    // State transition: rest of the rows and columns\n    for (i in 1..n) {\n        for (j in 1..m) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 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
    ### Edit distance: dynamic programming ###\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  # State transition: first row and first column\n  (1...(n + 1)).each { |i| dp[i][0] = i }\n  (1...(m + 1)).each { |j| dp[0][j] = j }\n  # State transition: rest of the rows and columns\n  for i in 1...(n + 1)\n    for j in 1...(m +1)\n      if s[i - 1] == t[j - 1]\n        # If two characters are equal, skip both characters\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # Minimum edit steps = minimum edit steps of insert, delete, replace + 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

    As shown in Figure 14-30, the state transition process for the edit distance problem is very similar to that of the knapsack problem; both can be viewed as the process of filling a two-dimensional grid.

    <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

    Figure 14-30   Dynamic programming process for edit distance

    ","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3-space-optimization","level":3,"title":"3.   Space Optimization","text":"

    Since \\(dp[i, j]\\) depends on the states above \\(dp[i-1, j]\\), to the left \\(dp[i, j-1]\\), and at the upper-left \\(dp[i-1, j-1]\\), forward traversal will lose the upper-left state \\(dp[i-1, j-1]\\), while reverse traversal cannot construct \\(dp[i, j-1]\\) in advance, so neither traversal order is suitable.

    For this reason, we can use a variable leftup to temporarily store the upper-left solution \\(dp[i-1, j-1]\\), so we only need to consider the solutions to the left and above. This situation is the same as in the unbounded knapsack problem, so we can use forward traversal. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
    def edit_distance_dp_comp(s: str, t: str) -> int:\n    \"\"\"Edit distance: Space-optimized dynamic programming\"\"\"\n    n, m = len(s), len(t)\n    dp = [0] * (m + 1)\n    # State transition: first row\n    for j in range(1, m + 1):\n        dp[j] = j\n    # State transition: rest of the rows\n    for i in range(1, n + 1):\n        # State transition: first column\n        leftup = dp[0]  # Temporarily store dp[i-1, j-1]\n        dp[0] += 1\n        # State transition: rest of the columns\n        for j in range(1, m + 1):\n            temp = dp[j]\n            if s[i - 1] == t[j - 1]:\n                # If two characters are equal, skip both characters\n                dp[j] = leftup\n            else:\n                # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n            leftup = temp  # Update for next round's dp[i-1, j-1]\n    return dp[m]\n
    edit_distance.cpp
    /* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<int> dp(m + 1, 0);\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.java
    /* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[] dp = new int[m + 1];\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.cs
    /* Edit distance: Space-optimized dynamic programming */\nint EditDistanceDPComp(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[] dp = new int[m + 1];\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.go
    /* Edit distance: Space-optimized dynamic programming */\nfunc editDistanceDPComp(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([]int, m+1)\n    // State transition: first row\n    for j := 1; j <= m; j++ {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for i := 1; i <= n; i++ {\n        // State transition: first column\n        leftUp := dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for j := 1; j <= m; j++ {\n            temp := dp[j]\n            if s[i-1] == t[j-1] {\n                // If two characters are equal, skip both characters\n                dp[j] = leftUp\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n            }\n            leftUp = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
    edit_distance.swift
    /* Edit distance: Space-optimized dynamic programming */\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    // State transition: first row\n    for j in 1 ... m {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for i in 1 ... n {\n        // State transition: first column\n        var leftup = dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for j in 1 ... m {\n            let temp = dp[j]\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
    edit_distance.js
    /* Edit distance: Space-optimized dynamic programming */\nfunction editDistanceDPComp(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // State transition: first row\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i <= n; i++) {\n        // State transition: first column\n        let leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.ts
    /* Edit distance: Space-optimized dynamic programming */\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    // State transition: first row\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i <= n; i++) {\n        // State transition: first column\n        let leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
    edit_distance.dart
    /* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(String s, String t) {\n  int n = s.length, m = t.length;\n  List<int> dp = List.filled(m + 1, 0);\n  // State transition: first row\n  for (int j = 1; j <= m; j++) {\n    dp[j] = j;\n  }\n  // State transition: rest of the rows\n  for (int i = 1; i <= n; i++) {\n    // State transition: first column\n    int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n    dp[0] = i;\n    // State transition: rest of the columns\n    for (int j = 1; j <= m; j++) {\n      int temp = dp[j];\n      if (s[i - 1] == t[j - 1]) {\n        // If two characters are equal, skip both characters\n        dp[j] = leftup;\n      } else {\n        // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n      }\n      leftup = temp; // Update for next round's dp[i-1, j-1]\n    }\n  }\n  return dp[m];\n}\n
    edit_distance.rs
    /* Edit distance: Space-optimized dynamic programming */\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    // State transition: first row\n    for j in 1..m {\n        dp[j] = j as i32;\n    }\n    // State transition: rest of the rows\n    for i in 1..=n {\n        // State transition: first column\n        let mut leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i as i32;\n        // State transition: rest of the columns\n        for j in 1..=m {\n            let temp = dp[j];\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    dp[m]\n}\n
    edit_distance.c
    /* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n    int *dp = calloc(m + 1, sizeof(int));\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    int res = dp[m];\n    // Free memory\n    free(dp);\n    return res;\n}\n
    edit_distance.kt
    /* Edit distance: Space-optimized dynamic programming */\nfun editDistanceDPComp(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = IntArray(m + 1)\n    // State transition: first row\n    for (j in 1..m) {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for (i in 1..n) {\n        // State transition: first column\n        var leftup = dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for (j in 1..m) {\n            val temp = dp[j]\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
    edit_distance.rb
    ### Edit distance: space-optimized DP ###\ndef edit_distance_dp_comp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(m + 1, 0)\n  # State transition: first row\n  (1...(m + 1)).each { |j| dp[j] = j }\n  # State transition: rest of the rows\n  for i in 1...(n + 1)\n    # State transition: first column\n    leftup = dp.first # Temporarily store dp[i-1, j-1]\n    dp[0] += 1\n    # State transition: rest of the columns\n    for j in 1...(m + 1)\n      temp = dp[j]\n      if s[i - 1] == t[j - 1]\n        # If two characters are equal, skip both characters\n        dp[j] = leftup\n      else\n        # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n      end\n      leftup = temp # Update for next round's dp[i-1, j-1]\n    end\n  end\n  dp[m]\nend\n
    ","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   Introduction to Dynamic Programming","text":"

    Dynamic programming is an important algorithmic paradigm that decomposes a problem into a series of smaller subproblems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving time efficiency.

    In this section, we start with a classic example, first presenting its brute force backtracking solution, observing the overlapping subproblems within it, and then gradually deriving a more efficient dynamic programming solution.

    Climbing stairs

    Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time, how many different ways are there to reach the top?

    As shown in Figure 14-1, for a \\(3\\)-step staircase, there are \\(3\\) different ways to reach the top.

    Figure 14-1   Number of ways to reach the 3rd step

    The goal of this problem is to determine the number of ways, so we can consider using backtracking to enumerate all possibilities. Specifically, imagine climbing stairs as a multi-round selection process: starting from the ground, choosing to go up \\(1\\) or \\(2\\) steps in each round, incrementing the count by \\(1\\) whenever the top of the stairs is reached, and pruning when exceeding the top. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py
    def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n    \"\"\"Backtracking\"\"\"\n    # When climbing to the n-th stair, add 1 to the solution count\n    if state == n:\n        res[0] += 1\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n:\n            continue\n        # Attempt: make a choice, update state\n        backtrack(choices, state + choice, n, res)\n        # Backtrack\n\ndef climbing_stairs_backtrack(n: int) -> int:\n    \"\"\"Climbing stairs: Backtracking\"\"\"\n    choices = [1, 2]  # Can choose to climb up 1 or 2 stairs\n    state = 0  # Start climbing from the 0-th stair\n    res = [0]  # Use res[0] to record the solution count\n    backtrack(choices, state, n, res)\n    return res[0]\n
    climbing_stairs_backtrack.cpp
    /* Backtracking */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    for (auto &choice : choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    vector<int> choices = {1, 2}; // Can choose to climb up 1 or 2 stairs\n    int state = 0;                // Start climbing from the 0-th stair\n    vector<int> res = {0};        // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res[0];\n}\n
    climbing_stairs_backtrack.java
    /* Backtracking */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (Integer choice : choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    List<Integer> choices = Arrays.asList(1, 2); // Can choose to climb up 1 or 2 stairs\n    int state = 0; // Start climbing from the 0-th stair\n    List<Integer> res = new ArrayList<>();\n    res.add(0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
    climbing_stairs_backtrack.cs
    /* Backtracking */\nvoid Backtrack(List<int> choices, int state, int n, List<int> res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    foreach (int choice in choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        Backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint ClimbingStairsBacktrack(int n) {\n    List<int> choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    int state = 0; // Start climbing from the 0-th stair\n    List<int> res = [0]; // Use res[0] to record the solution count\n    Backtrack(choices, state, n, res);\n    return res[0];\n}\n
    climbing_stairs_backtrack.go
    /* Backtracking */\nfunc backtrack(choices []int, state, n int, res []int) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] = res[0] + 1\n    }\n    // Traverse all choices\n    for _, choice := range choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state+choice > n {\n            continue\n        }\n        // Attempt: make choice, update state\n        backtrack(choices, state+choice, n, res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunc climbingStairsBacktrack(n int) int {\n    // Can choose to climb up 1 or 2 stairs\n    choices := []int{1, 2}\n    // Start climbing from the 0-th stair\n    state := 0\n    res := make([]int, 1)\n    // Use res[0] to record the solution count\n    res[0] = 0\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
    climbing_stairs_backtrack.swift
    /* Backtracking */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] += 1\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n {\n            continue\n        }\n        // Attempt: make choice, update state\n        backtrack(choices: choices, state: state + choice, n: n, res: &res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunc climbingStairsBacktrack(n: Int) -> Int {\n    let choices = [1, 2] // Can choose to climb up 1 or 2 stairs\n    let state = 0 // Start climbing from the 0-th stair\n    var res: [Int] = []\n    res.append(0) // Use res[0] to record the solution count\n    backtrack(choices: choices, state: state, n: n, res: &res)\n    return res[0]\n}\n
    climbing_stairs_backtrack.js
    /* Backtracking */\nfunction backtrack(choices, state, n, res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunction climbingStairsBacktrack(n) {\n    const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    const state = 0; // Start climbing from the 0-th stair\n    const res = new Map();\n    res.set(0, 0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
    climbing_stairs_backtrack.ts
    /* Backtracking */\nfunction backtrack(\n    choices: number[],\n    state: number,\n    n: number,\n    res: Map<0, any>\n): void {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunction climbingStairsBacktrack(n: number): number {\n    const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    const state = 0; // Start climbing from the 0-th stair\n    const res = new Map();\n    res.set(0, 0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
    climbing_stairs_backtrack.dart
    /* Backtracking */\nvoid backtrack(List<int> choices, int state, int n, List<int> res) {\n  // When climbing to the n-th stair, add 1 to the solution count\n  if (state == n) {\n    res[0]++;\n  }\n  // Traverse all choices\n  for (int choice in choices) {\n    // Pruning: not allowed to go beyond the n-th stair\n    if (state + choice > n) continue;\n    // Attempt: make choice, update state\n    backtrack(choices, state + choice, n, res);\n    // Backtrack\n  }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n  List<int> choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n  int state = 0; // Start climbing from the 0-th stair\n  List<int> res = [];\n  res.add(0); // Use res[0] to record the solution count\n  backtrack(choices, state, n, res);\n  return res[0];\n}\n
    climbing_stairs_backtrack.rs
    /* Backtracking */\nfn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] = res[0] + 1;\n    }\n    // Traverse all choices\n    for &choice in choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n {\n            continue;\n        }\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfn climbing_stairs_backtrack(n: usize) -> i32 {\n    let choices = vec![1, 2]; // Can choose to climb up 1 or 2 stairs\n    let state = 0; // Start climbing from the 0-th stair\n    let mut res = Vec::new();\n    res.push(0); // Use res[0] to record the solution count\n    backtrack(&choices, state, n as i32, &mut res);\n    res[0]\n}\n
    climbing_stairs_backtrack.c
    /* Backtracking */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    for (int i = 0; i < len; i++) {\n        int choice = choices[i];\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res, len);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    int choices[2] = {1, 2}; // Can choose to climb up 1 or 2 stairs\n    int state = 0;           // Start climbing from the 0-th stair\n    int *res = (int *)malloc(sizeof(int));\n    *res = 0; // Use res[0] to record the solution count\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
    /* Backtracking */\nfun backtrack(\n    choices: MutableList<Int>,\n    state: Int,\n    n: Int,\n    res: MutableList<Int>\n) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0] = res[0] + 1\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfun climbingStairsBacktrack(n: Int): Int {\n    val choices = mutableListOf(1, 2) // Can choose to climb up 1 or 2 stairs\n    val state = 0 // Start climbing from the 0-th stair\n    val res = mutableListOf<Int>()\n    res.add(0) // Use res[0] to record the solution count\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
    climbing_stairs_backtrack.rb
    ### Backtracking ###\ndef backtrack(choices, state, n, res)\n  # When climbing to the n-th stair, add 1 to the solution count\n  res[0] += 1 if state == n\n  # Traverse all choices\n  for choice in choices\n    # Pruning: not allowed to go beyond the n-th stair\n    next if state + choice > n\n\n    # Attempt: make choice, update state\n    backtrack(choices, state + choice, n, res)\n  end\n  # Backtrack\nend\n\n### Climbing stairs: backtracking ###\ndef climbing_stairs_backtrack(n)\n  choices = [1, 2] # Can choose to climb up 1 or 2 stairs\n  state = 0 # Start climbing from the 0-th stair\n  res = [0] # Use res[0] to record the solution count\n  backtrack(choices, state, n, res)\n  res.first\nend\n
    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-method-1-brute-force-search","level":2,"title":"14.1.1   Method 1: Brute Force Search","text":"

    Backtracking algorithms typically do not explicitly decompose problems, but rather treat solving the problem as a series of decision steps, searching for all possible solutions through trial and pruning.

    We can try to analyze this problem from the perspective of problem decomposition. Let the number of ways to climb to the \\(i\\)-th step be \\(dp[i]\\), then \\(dp[i]\\) is the original problem, and its subproblems include:

    \\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\]

    Since we can only go up \\(1\\) or \\(2\\) steps in each round, when we stand on the \\(i\\)-th step, we could only have been on the \\(i-1\\)-th or \\(i-2\\)-th step in the previous round. In other words, we can only reach the \\(i\\)-th step from the \\(i-1\\)-th or \\(i-2\\)-th step.

    This leads to an important conclusion: the number of ways to climb to the \\(i-1\\)-th step plus the number of ways to climb to the \\(i-2\\)-th step equals the number of ways to climb to the \\(i\\)-th step. The formula is as follows:

    \\[ dp[i] = dp[i-1] + dp[i-2] \\]

    This means that in the stair climbing problem, there exists a recurrence relation among the subproblems, and the solution to the original problem can be constructed from the solutions to the subproblems. Figure 14-2 illustrates this recurrence relation.

    Figure 14-2   Recurrence relation for the number of ways

    We can obtain a brute force search solution based on the recurrence formula. Starting from \\(dp[n]\\), recursively decompose a larger problem into the sum of two smaller problems, until reaching the smallest subproblems \\(dp[1]\\) and \\(dp[2]\\) and returning. Among them, the solutions to the smallest subproblems are known, namely \\(dp[1] = 1\\) and \\(dp[2] = 2\\), representing \\(1\\) and \\(2\\) ways to climb to the \\(1\\)st and \\(2\\)nd steps, respectively.

    Observe the following code: like standard backtracking code, it also uses depth-first search but is more concise:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py
    def dfs(i: int) -> int:\n    \"\"\"Search\"\"\"\n    # Known dp[1] and dp[2], return them\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    \"\"\"Climbing stairs: Search\"\"\"\n    return dfs(n)\n
    climbing_stairs_dfs.cpp
    /* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.java
    /* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.cs
    /* Search */\nint DFS(int i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint ClimbingStairsDFS(int n) {\n    return DFS(n);\n}\n
    climbing_stairs_dfs.go
    /* Search */\nfunc dfs(i int) int {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfunc climbingStairsDFS(n int) int {\n    return dfs(n)\n}\n
    climbing_stairs_dfs.swift
    /* Search */\nfunc dfs(i: Int) -> Int {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfunc climbingStairsDFS(n: Int) -> Int {\n    dfs(i: n)\n}\n
    climbing_stairs_dfs.js
    /* Search */\nfunction dfs(i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfunction climbingStairsDFS(n) {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.ts
    /* Search */\nfunction dfs(i: number): number {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfunction climbingStairsDFS(n: number): number {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.dart
    /* Search */\nint dfs(int i) {\n  // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n  return dfs(n);\n}\n
    climbing_stairs_dfs.rs
    /* Search */\nfn dfs(i: usize) -> i32 {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfn climbing_stairs_dfs(n: usize) -> i32 {\n    dfs(n)\n}\n
    climbing_stairs_dfs.c
    /* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
    climbing_stairs_dfs.kt
    /* Search */\nfun dfs(i: Int): Int {\n    // Known dp[1] and dp[2], return them\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/* Climbing stairs: Search */\nfun climbingStairsDFS(n: Int): Int {\n    return dfs(n)\n}\n
    climbing_stairs_dfs.rb
    ### Search ###\ndef dfs(i)\n  # Known dp[1] and dp[2], return them\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### Climbing stairs: search ###\ndef climbing_stairs_dfs(n)\n  dfs(n)\nend\n

    Figure 14-3 shows the recursion tree formed by brute force search. For the problem \\(dp[n]\\), the depth of its recursion tree is \\(n\\), with a time complexity of \\(O(2^n)\\). Exponential growth is explosive; if we input a relatively large \\(n\\), the wait can be very long.

    Figure 14-3   Recursion tree for climbing stairs

    Observing the above figure, the exponential time complexity is caused by \"overlapping subproblems\". For example, \\(dp[9]\\) is decomposed into \\(dp[8]\\) and \\(dp[7]\\), and \\(dp[8]\\) is decomposed into \\(dp[7]\\) and \\(dp[6]\\), both of which contain the subproblem \\(dp[7]\\).

    And so on, subproblems contain smaller overlapping subproblems, ad infinitum. The vast majority of computational resources are wasted on these overlapping subproblems.

    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-method-2-memoization","level":2,"title":"14.1.2   Method 2: Memoization","text":"

    To improve algorithm efficiency, we want all overlapping subproblems to be computed only once. For this purpose, we declare an array mem to record the solution to each subproblem and prune overlapping subproblems during the search process.

    1. When computing \\(dp[i]\\) for the first time, we record it in mem[i] for later use.
    2. When we need to compute \\(dp[i]\\) again, we can directly retrieve the result from mem[i], thereby avoiding redundant computation of that subproblem.

    The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py
    def dfs(i: int, mem: list[int]) -> int:\n    \"\"\"Memoization search\"\"\"\n    # Known dp[1] and dp[2], return them\n    if i == 1 or i == 2:\n        return i\n    # If record dp[i] exists, return it directly\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    # Record dp[i]\n    mem[i] = count\n    return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n    \"\"\"Climbing stairs: Memoization search\"\"\"\n    # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    mem = [-1] * (n + 1)\n    return dfs(n, mem)\n
    climbing_stairs_dfs_mem.cpp
    /* Memoization search */\nint dfs(int i, vector<int> &mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    vector<int> mem(n + 1, -1);\n    return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.java
    /* Memoization search */\nint dfs(int i, int[] mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int[] mem = new int[n + 1];\n    Arrays.fill(mem, -1);\n    return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.cs
    /* Memoization search */\nint DFS(int i, int[] mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint ClimbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int[] mem = new int[n + 1];\n    Array.Fill(mem, -1);\n    return DFS(n, mem);\n}\n
    climbing_stairs_dfs_mem.go
    /* Memoization search */\nfunc dfsMem(i int, mem []int) int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfunc climbingStairsDFSMem(n int) int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\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
    /* Memoization search */\nfunc dfs(i: Int, mem: inout [Int]) -> Int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfunc climbingStairsDFSMem(n: Int) -> Int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    var mem = Array(repeating: -1, count: n + 1)\n    return dfs(i: n, mem: &mem)\n}\n
    climbing_stairs_dfs_mem.js
    /* Memoization search */\nfunction dfs(i, mem) {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nfunction climbingStairsDFSMem(n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.ts
    /* Memoization search */\nfunction dfs(i: number, mem: number[]): number {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nfunction climbingStairsDFSMem(n: number): number {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.dart
    /* Memoization search */\nint dfs(int i, List<int> mem) {\n  // Known dp[1] and dp[2], return them\n  if (i == 1 || i == 2) return i;\n  // If record dp[i] exists, return it directly\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  // Record dp[i]\n  mem[i] = count;\n  return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n  // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n  List<int> mem = List.filled(n + 1, -1);\n  return dfs(n, mem);\n}\n
    climbing_stairs_dfs_mem.rs
    /* Memoization search */\nfn dfs(i: usize, mem: &mut [i32]) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    count\n}\n\n/* Climbing stairs: Memoization search */\nfn climbing_stairs_dfs_mem(n: usize) -> i32 {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    let mut mem = vec![-1; n + 1];\n    dfs(n, &mut mem)\n}\n
    climbing_stairs_dfs_mem.c
    /* Memoization search */\nint dfs(int i, int *mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\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
    /* Memoization search */\nfun dfs(i: Int, mem: IntArray): Int {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2) return i\n    // If record dp[i] exists, return it directly\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    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfun climbingStairsDFSMem(n: Int): Int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    val mem = IntArray(n + 1)\n    mem.fill(-1)\n    return dfs(n, mem)\n}\n
    climbing_stairs_dfs_mem.rb
    ### Memoization search ###\ndef dfs(i, mem)\n  # Known dp[1] and dp[2], return them\n  return i if i == 1 || i == 2\n  # If record dp[i] exists, return it directly\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  # Record dp[i]\n  mem[i] = count\nend\n\n### Climbing stairs: memoization search ###\ndef climbing_stairs_dfs_mem(n)\n  # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n  mem = Array.new(n + 1, -1)\n  dfs(n, mem)\nend\n

    Observe Figure 14-4: after memoization, all overlapping subproblems need to be computed only once, reducing the time complexity to \\(O(n)\\), which is a tremendous leap.

    Figure 14-4   Recursion tree with memoization

    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-method-3-dynamic-programming","level":2,"title":"14.1.3   Method 3: Dynamic Programming","text":"

    Memoization is a \"top-down\" method: we start from the original problem (root node), recursively decompose larger subproblems into smaller ones, until reaching the smallest known subproblems (leaf nodes). Afterward, by backtracking, we collect the solutions to the subproblems layer by layer to construct the solution to the original problem.

    In contrast, dynamic programming is a \"bottom-up\" method: starting from the solutions to the smallest subproblems, iteratively constructing solutions to larger subproblems until obtaining the solution to the original problem.

    Since dynamic programming does not include a backtracking process, it only requires loop iteration for implementation and does not need recursion. In the following code, we initialize an array dp to store the solutions to subproblems, which serves the same recording function as the array mem in memoization:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
    def climbing_stairs_dp(n: int) -> int:\n    \"\"\"Climbing stairs: Dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return n\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [0] * (n + 1)\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1], dp[2] = 1, 2\n    # State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    vector<int> dp(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint ClimbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfunc climbingStairsDP(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfunc climbingStairsDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: 0, count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfunction climbingStairsDP(n) {\n    if (n === 1 || n === 2) return n;\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1).fill(-1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfunction climbingStairsDP(n: number): number {\n    if (n === 1 || n === 2) return n;\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1).fill(-1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n  if (n == 1 || n == 2) return n;\n  // Initialize dp table, used to store solutions to subproblems\n  List<int> dp = List.filled(n + 1, 0);\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1] = 1;\n  dp[2] = 2;\n  // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfn climbing_stairs_dp(n: usize) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![-1; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int *dp = (int *)malloc((n + 1) * sizeof(int));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\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
    /* Climbing stairs: Dynamic programming */\nfun climbingStairsDP(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = IntArray(n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\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
    ### Climbing stairs: dynamic programming ###\ndef climbing_stairs_dp(n)\n  return n  if n == 1 || n == 2\n\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = 1, 2\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n  dp[n]\nend\n

    Figure 14-5 simulates the execution process of the above code.

    Figure 14-5   Dynamic programming process for climbing stairs

    Like backtracking algorithms, dynamic programming also uses the \"state\" concept to represent specific stages of problem solving, with each state corresponding to a subproblem and its corresponding local optimal solution. For example, the state in the stair climbing problem is defined as the current stair step number \\(i\\).

    Based on the above content, we can summarize the commonly used terminology in dynamic programming.

    • The array dp is called the dp table, where \\(dp[i]\\) represents the solution to the subproblem corresponding to state \\(i\\).
    • The states corresponding to the smallest subproblems (the \\(1\\)st and \\(2\\)nd steps) are called initial states.
    • The recurrence formula \\(dp[i] = dp[i-1] + dp[i-2]\\) is called the state transition equation.
    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414-space-optimization","level":2,"title":"14.1.4   Space Optimization","text":"

    Observant readers may have noticed that since \\(dp[i]\\) is only related to \\(dp[i-1]\\) and \\(dp[i-2]\\), we do not need to use an array dp to store the solutions to all subproblems, and can instead use two variables that roll forward. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
    def climbing_stairs_dp_comp(n: int) -> int:\n    \"\"\"Climbing stairs: Space-optimized dynamic programming\"\"\"\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\nfunc climbingStairsDPComp(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    a, b := 1, 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        a, b = b, a+b\n    }\n    return b\n}\n
    climbing_stairs_dp.swift
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    /* Climbing stairs: Space-optimized dynamic programming */\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
    ### Climbing stairs: space-optimized DP ###\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

    As the above code shows, by eliminating the space occupied by the array dp, the space complexity is reduced from \\(O(n)\\) to \\(O(1)\\).

    In dynamic programming problems, the current state often depends only on a limited number of preceding states, allowing us to retain only the necessary states and save memory space through \"dimension reduction\". This space optimization technique is called \"rolling variable\" or \"rolling array\".

    ","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   0-1 Knapsack Problem","text":"

    The knapsack problem is an excellent introductory problem for dynamic programming and is one of the most common problem forms in dynamic programming. It has many variants, such as the 0-1 knapsack problem, the unbounded knapsack problem, and the multiple knapsack problem.

    In this section, we will first solve the most common 0-1 knapsack problem.

    Question

    Given \\(n\\) items and a knapsack with capacity \\(cap\\), where the weight and value of the \\(i\\)-th item are \\(wgt[i-1]\\) and \\(val[i-1]\\), respectively. Each item can be selected at most once. What is the maximum value that can fit in the knapsack under the capacity limit?

    Observe Figure 14-17. Since item number \\(i\\) starts counting from \\(1\\) and array indices start from \\(0\\), item \\(i\\) corresponds to weight \\(wgt[i-1]\\) and value \\(val[i-1]\\).

    Figure 14-17   Example data for 0-1 knapsack

    We can view the 0-1 knapsack problem as a process consisting of \\(n\\) rounds of decisions, where for each item there are two decisions: not putting it in and putting it in, thus the problem satisfies the decision tree model.

    The goal of this problem is to find \"the maximum value that can be placed in the knapsack within the capacity limit\", so it is more likely to be a dynamic programming problem.

    Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

    For each item, if not placed in the knapsack, the knapsack capacity remains unchanged; if placed in, the knapsack capacity decreases. From this, we can derive the state definition: current item number \\(i\\) and knapsack capacity \\(c\\), denoted as \\([i, c]\\).

    State \\([i, c]\\) corresponds to the subproblem: the maximum value among the first \\(i\\) items in a knapsack of capacity \\(c\\), denoted as \\(dp[i, c]\\).

    What we need to find is \\(dp[n, cap]\\), so we need a two-dimensional \\(dp\\) table of size \\((n+1) \\times (cap+1)\\).

    Step 2: Identify the optimal substructure, and then derive the state transition equation

    After making the decision for item \\(i\\), what remains is the subproblem of the first \\(i-1\\) items, which can be divided into the following two cases.

    • Not putting item \\(i\\): The knapsack capacity remains unchanged, and the state changes to \\([i-1, c]\\).
    • Putting item \\(i\\): The knapsack capacity decreases by \\(wgt[i-1]\\), the value increases by \\(val[i-1]\\), and the state changes to \\([i-1, c-wgt[i-1]]\\).

    The above analysis reveals the optimal substructure of this problem: the maximum value \\(dp[i, c]\\) equals the greater of the values obtained by not putting item \\(i\\) into the knapsack and by putting it into the knapsack. From this, the state transition equation can be derived:

    \\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\]

    Note that if the weight of the current item \\(wgt[i - 1]\\) exceeds the remaining knapsack capacity \\(c\\), then the only option is not to put it in the knapsack.

    Step 3: Determine boundary conditions and state transition order

    When there are no items or the knapsack capacity is \\(0\\), the maximum value is \\(0\\), i.e., the first column \\(dp[i, 0]\\) and the first row \\(dp[0, c]\\) are both equal to \\(0\\).

    The current state \\([i, c]\\) transitions from the state above \\([i-1, c]\\) and the upper-left state \\([i-1, c-wgt[i-1]]\\), so we can traverse the entire \\(dp\\) table in forward order using two nested loops.

    Based on the above analysis, we will next implement the brute force search, memoization, and dynamic programming solutions in order.

    ","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-method-1-brute-force-search","level":3,"title":"1.   Method 1: Brute Force Search","text":"

    The search code includes the following elements.

    • Recursive parameters: state \\([i, c]\\).
    • Return value: solution to the subproblem \\(dp[i, c]\\).
    • Termination condition: when there are no items left (\\(i = 0\\)) or the remaining knapsack capacity is \\(0\\), terminate the recursion and return value \\(0\\).
    • Pruning: if the weight of the current item exceeds the remaining knapsack capacity, only the option of not putting it in is available.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
    def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n    \"\"\"0-1 knapsack: Brute-force search\"\"\"\n    # If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 or c == 0:\n        return 0\n    # If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c:\n        return knapsack_dfs(wgt, val, i - 1, c)\n    # Calculate the maximum value of not putting in and putting in item 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    # Return the larger value of the two options\n    return max(no, yes)\n
    knapsack.cpp
    /* 0-1 knapsack: Brute-force search */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return max(no, yes);\n}\n
    knapsack.java
    /* 0-1 knapsack: Brute-force search */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
    knapsack.cs
    /* 0-1 knapsack: Brute-force search */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (weight[i - 1] > c) {\n        return KnapsackDFS(weight, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return Math.Max(no, yes);\n}\n
    knapsack.go
    /* 0-1 knapsack: Brute-force search */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i-1] > c {\n        return knapsackDFS(wgt, val, i-1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    no := knapsackDFS(wgt, val, i-1, c)\n    yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n    // Return the larger value of the two options\n    return int(math.Max(float64(no), float64(yes)))\n}\n
    knapsack.swift
    /* 0-1 knapsack: Brute-force search */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c {\n        return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return max(no, yes)\n}\n
    knapsack.js
    /* 0-1 knapsack: Brute-force search */\nfunction knapsackDFS(wgt, val, i, c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
    knapsack.ts
    /* 0-1 knapsack: Brute-force search */\nfunction knapsackDFS(\n    wgt: Array<number>,\n    val: Array<number>,\n    i: number,\n    c: number\n): number {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
    knapsack.dart
    /* 0-1 knapsack: Brute-force search */\nint knapsackDFS(List<int> wgt, List<int> val, int i, int c) {\n  // If all items have been selected or knapsack has no remaining capacity, return value 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // If exceeds knapsack capacity, can only choose not to put it in\n  if (wgt[i - 1] > c) {\n    return knapsackDFS(wgt, val, i - 1, c);\n  }\n  // Calculate the maximum value of not putting in and putting in item 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  // Return the larger value of the two options\n  return max(no, yes);\n}\n
    knapsack.rs
    /* 0-1 knapsack: Brute-force search */\nfn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    std::cmp::max(no, yes)\n}\n
    knapsack.c
    /* 0-1 knapsack: Brute-force search */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return myMax(no, yes);\n}\n
    knapsack.kt
    /* 0-1 knapsack: Brute-force search */\nfun knapsackDFS(\n    wgt: IntArray,\n    _val: IntArray,\n    i: Int,\n    c: Int\n): Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, _val, i - 1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    return max(no, yes)\n}\n
    knapsack.rb
    ### 0-1 knapsack: brute force search ###\ndef knapsack_dfs(wgt, val, i, c)\n  # If all items have been selected or knapsack has no remaining capacity, return value 0\n  return 0 if i == 0 || c == 0\n  # If exceeds knapsack capacity, can only choose not to put it in\n  return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c\n  # Calculate the maximum value of not putting in and putting in item 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  # Return the larger value of the two options\n  [no, yes].max\nend\n

    As shown in Figure 14-18, since each item generates two search branches, excluding it and including it, the time complexity is \\(O(2^n)\\).

    Observing the recursion tree, it is easy to see overlapping subproblems, such as \\(dp[1, 10]\\). When there are many items, large knapsack capacity, and especially many items with the same weight, the number of overlapping subproblems will increase significantly.

    Figure 14-18   Brute force search recursion tree for 0-1 knapsack problem

    ","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-method-2-memoization","level":3,"title":"2.   Method 2: Memoization","text":"

    To ensure that overlapping subproblems are only computed once, we use a memo list mem to record the solutions to subproblems, where mem[i][c] corresponds to \\(dp[i, c]\\).

    After introducing memoization, the time complexity depends on the number of subproblems, which is \\(O(n \\times cap)\\). The implementation code is as follows:

    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 knapsack: Memoization search\"\"\"\n    # If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 or c == 0:\n        return 0\n    # If there's a record, return it directly\n    if mem[i][c] != -1:\n        return mem[i][c]\n    # If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c:\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    # Calculate the maximum value of not putting in and putting in item 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    # Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n
    knapsack.cpp
    /* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.java
    /* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.cs
    /* 0-1 knapsack: Memoization search */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (weight[i - 1] > c) {\n        return KnapsackDFSMem(weight, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = Math.Max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.go
    /* 0-1 knapsack: Memoization search */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i-1] > c {\n        return knapsackDFSMem(wgt, val, mem, i-1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Return the larger value of the two options\n    mem[i][c] = int(math.Max(float64(no), float64(yes)))\n    return mem[i][c]\n}\n
    knapsack.swift
    /* 0-1 knapsack: Memoization search */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c {\n        return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
    knapsack.js
    /* 0-1 knapsack: Memoization search */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.ts
    /* 0-1 knapsack: Memoization search */\nfunction knapsackDFSMem(\n    wgt: Array<number>,\n    val: Array<number>,\n    mem: Array<Array<number>>,\n    i: number,\n    c: number\n): number {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
    knapsack.dart
    /* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(\n  List<int> wgt,\n  List<int> val,\n  List<List<int>> mem,\n  int i,\n  int c,\n) {\n  // If all items have been selected or knapsack has no remaining capacity, return value 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // If there's a record, return it directly\n  if (mem[i][c] != -1) {\n    return mem[i][c];\n  }\n  // If exceeds knapsack capacity, can only choose not to put it in\n  if (wgt[i - 1] > c) {\n    return knapsackDFSMem(wgt, val, mem, i - 1, c);\n  }\n  // Calculate the maximum value of not putting in and putting in item 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  // Record and return the larger value of the two options\n  mem[i][c] = max(no, yes);\n  return mem[i][c];\n}\n
    knapsack.rs
    /* 0-1 knapsack: Memoization search */\nfn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = std::cmp::max(no, yes);\n    mem[i][c]\n}\n
    knapsack.c
    /* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = myMax(no, yes);\n    return mem[i][c];\n}\n
    knapsack.kt
    /* 0-1 knapsack: Memoization search */\nfun knapsackDFSMem(\n    wgt: IntArray,\n    _val: IntArray,\n    mem: Array<IntArray>,\n    i: Int,\n    c: Int\n): Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item 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    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
    knapsack.rb
    ### 0-1 knapsack: memoization search ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n  # If all items have been selected or knapsack has no remaining capacity, return value 0\n  return 0 if i == 0 || c == 0\n  # If there's a record, return it directly\n  return mem[i][c] if mem[i][c] != -1\n  # If exceeds knapsack capacity, can only choose not to put it in\n  return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c\n  # Calculate the maximum value of not putting in and putting in item 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  # Record and return the larger value of the two options\n  mem[i][c] = [no, yes].max\nend\n

    Figure 14-19 shows the search branches pruned in memoization.

    Figure 14-19   Memoization recursion tree for 0-1 knapsack problem

    ","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-method-3-dynamic-programming","level":3,"title":"3.   Method 3: Dynamic Programming","text":"

    Dynamic programming is essentially the process of filling the \\(dp\\) table during state transitions. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
    def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 knapsack: Dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # State transition\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (weight[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfunc knapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfunction knapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(n + 1)\n        .fill(0)\n        .map(() => Array(cap + 1).fill(0));\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfunction knapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint knapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nfn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Dynamic programming */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\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    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
    knapsack.kt
    /* 0-1 knapsack: Dynamic programming */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: dynamic programming ###\ndef knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # State transition\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # The larger value between not selecting and selecting item 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

    As shown in Figure 14-20, both time complexity and space complexity are determined by the size of the array dp, which is \\(O(n \\times cap)\\).

    <1><2><3><4><5><6><7><8><9><10><11><12><13><14>

    Figure 14-20   Dynamic programming process for 0-1 knapsack problem

    ","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4-space-optimization","level":3,"title":"4.   Space Optimization","text":"

    Since each state is only related to the state in the row above it, we can use two arrays rolling forward to reduce the space complexity from \\(O(n^2)\\) to \\(O(n)\\).

    Further thinking, can we achieve space optimization using just one array? Observing, we can see that each state is transferred from the cell directly above or the cell in the upper-left. If there is only one array, when we start traversing row \\(i\\), that array still stores the state of row \\(i-1\\).

    • If using forward traversal, then when traversing to \\(dp[i, j]\\), the values in the upper-left \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) may have already been overwritten, thus preventing correct state transition.
    • If using reverse traversal, there will be no overwriting issue, and state transition can proceed correctly.

    Figure 14-21 shows the transition process from row \\(i = 1\\) to row \\(i = 2\\) using a single array. Please consider the difference between forward and reverse traversal.

    <1><2><3><4><5><6>

    Figure 14-21   Space-optimized dynamic programming process for 0-1 knapsack

    In the code implementation, we simply need to delete the first dimension \\(i\\) of the array dp and change the inner loop to reverse traversal:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
    def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 knapsack: Space-optimized dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [0] * (cap + 1)\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in reverse order\n        for c in range(cap, 0, -1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
    knapsack.cpp
    /* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<int> dp(cap + 1, 0);\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c > 0; c--) {\n            if (weight[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([]int, cap+1)\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in reverse order\n        for c := cap; c >= 1; c-- {\n            if wgt[i-1] <= c {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: cap + 1)\n    // State transition\n    for i in 1 ... n {\n        // Traverse in reverse order\n        for c in (1 ... cap).reversed() {\n            if wgt[i - 1] <= c {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfunction knapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(cap + 1).fill(0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfunction knapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(cap + 1).fill(0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<int> dp = List.filled(cap + 1, 0);\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    // Traverse in reverse order\n    for (int c = cap; c >= 1; c--) {\n      if (wgt[i - 1] <= c) {\n        // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nfn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![0; cap + 1];\n    // State transition\n    for i in 1..=n {\n        // Traverse in reverse order\n        for c in (1..=cap).rev() {\n            if wgt[i - 1] <= c as i32 {\n                // The larger value between not selecting and selecting item 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 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int *dp = calloc(cap + 1, sizeof(int));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Free memory\n    free(dp);\n    return res;\n}\n
    knapsack.kt
    /* 0-1 knapsack: Space-optimized dynamic programming */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = IntArray(cap + 1)\n    // State transition\n    for (i in 1..n) {\n        // Traverse in reverse order\n        for (c in cap downTo 1) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item 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 knapsack: space-optimized DP ###\ndef knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(cap + 1, 0)\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in reverse order\n    for c in cap.downto(1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c]\n      else\n        # The larger value between not selecting and selecting item 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":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   Summary","text":"","path":["Chapter 14. Dynamic Programming","14.7   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1-key-points","level":3,"title":"1.   Key Points","text":"
    • Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency.
    • Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once.
    • Memoization is a top-down recursive solution, while the corresponding dynamic programming is a bottom-up iterative solution, similar to \"filling in a table\". Since the current state only depends on certain local states, we can eliminate one dimension of the \\(dp\\) table to reduce space complexity.
    • Subproblem decomposition is a general algorithmic approach, with different properties in divide and conquer, dynamic programming, and backtracking.
    • Dynamic programming problems have three major characteristics: overlapping subproblems, optimal substructure, and no aftereffects.
    • If the optimal solution to the original problem can be constructed from the optimal solutions to the subproblems, then it has optimal substructure.
    • No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not satisfy this property and cannot be solved efficiently using dynamic programming.

    Knapsack problem

    • The knapsack problem is one of the most typical dynamic programming problems, with variants such as the 0-1 knapsack, unbounded knapsack, and multiple knapsack.
    • The state definition for the 0-1 knapsack is the maximum value achievable using the first \\(i\\) items with a knapsack capacity of \\(c\\). Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state.
    • The unbounded knapsack problem has no limit on the selection quantity of each type of item, so the state transition for choosing to put in an item differs from the 0-1 knapsack problem. Since the state depends on the state directly above and directly to the left, space optimization should use forward traversal.
    • The coin change problem is a variant of the unbounded knapsack problem. It changes from seeking the \"maximum\" value to seeking the \"minimum\" number of coins, so \\(\\max()\\) in the state transition equation should be changed to \\(\\min()\\). It changes from seeking \"not exceeding\" the knapsack capacity to seeking \"exactly\" making up the target amount, so \\(amt + 1\\) is used to represent the invalid solution of \"unable to make up the target amount\".
    • Coin change problem II changes from seeking the \"minimum number of coins\" to seeking the \"number of coin combinations\", so the state transition equation correspondingly changes from \\(\\min()\\) to a summation operator.

    Edit distance problem

    • Edit distance (Levenshtein distance) is used to measure the similarity between two strings, defined as the minimum number of edit steps from one string to another, with edit operations including insert, delete, and replace.
    • The state definition for the edit distance problem is the minimum number of edit steps required to change the first \\(i\\) characters of \\(s\\) into the first \\(j\\) characters of \\(t\\). When \\(s[i] \\ne t[j]\\), there are three decisions: insert, delete, replace, each with corresponding remaining subproblems. From this, the optimal substructure can be identified and the state transition equation constructed. When \\(s[i] = t[j]\\), no edit is required for the current character.
    • In edit distance, the state depends on the state directly above, directly to the left, and to the upper-left, so after space optimization, neither forward nor reverse traversal can correctly perform state transitions. For this reason, we use a variable to temporarily store the upper-left state, thus transforming to a situation equivalent to the unbounded knapsack problem, allowing for forward traversal after space optimization.
    ","path":["Chapter 14. Dynamic Programming","14.7   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   Unbounded Knapsack Problem","text":"

    In this section, we first solve another common knapsack problem: the unbounded knapsack, and then explore a special case of it: the coin change problem.

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451-unbounded-knapsack-problem","level":2,"title":"14.5.1   Unbounded Knapsack Problem","text":"

    Question

    Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can be selected multiple times. What is the maximum value that can be placed in the knapsack within the capacity limit? An example is shown in Figure 14-22.

    Figure 14-22   Example data for unbounded knapsack problem

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach","level":3,"title":"1.   Dynamic Programming Approach","text":"

    The unbounded knapsack problem is very similar to the 0-1 knapsack problem, differing only in that there is no limit on the number of times an item can be selected.

    • In the 0-1 knapsack problem, there is only one of each type of item, so after placing item \\(i\\) in the knapsack, we can only choose from the first \\(i-1\\) items.
    • In the unbounded knapsack problem, the quantity of each type of item is unlimited, so after placing item \\(i\\) in the knapsack, we can still choose from the first \\(i\\) items.

    Under the rules of the unbounded knapsack problem, the changes in state \\([i, c]\\) are divided into two cases.

    • Not putting item \\(i\\): Same as the 0-1 knapsack problem, transfer to \\([i-1, c]\\).
    • Putting item \\(i\\): Different from the 0-1 knapsack problem, transfer to \\([i, c-wgt[i-1]]\\).

    Thus, the state transition equation becomes:

    \\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    Comparing the code for the two problems, there is one change in state transition from \\(i-1\\) to \\(i\\), with everything else identical:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
    def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Unbounded knapsack: Dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # State transition\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfunction unboundedKnapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nfn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\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    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item 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    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
    unbounded_knapsack.kt
    /* Unbounded knapsack: Dynamic programming */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    ### Unbounded knapsack: dynamic programming ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # State transition\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # The larger value between not selecting and selecting item 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":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization","level":3,"title":"3.   Space Optimization","text":"

    Since the current state is transferred from states on the left and above, after space optimization, each row in the \\(dp\\) table should be traversed in forward order.

    This traversal order is exactly opposite to the 0-1 knapsack. Please refer to Figure 14-23 to understand the difference between the two.

    <1><2><3><4><5><6>

    Figure 14-23   Space-optimized dynamic programming process for unbounded knapsack problem

    The code implementation is relatively simple, just delete the first dimension of the array dp:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
    def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Unbounded knapsack: Space-optimized dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [0] * (cap + 1)\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
    unbounded_knapsack.cpp
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<int> dp(cap + 1, 0);\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([]int, cap+1)\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: cap + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfunction unboundedKnapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<int> dp = List.filled(cap + 1, 0);\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c];\n      } else {\n        // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![0; cap + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item 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
    /* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int *dp = calloc(cap + 1, sizeof(int));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Free memory\n    free(dp);\n    return res;\n}\n
    unbounded_knapsack.kt
    /* Unbounded knapsack: Space-optimized dynamic programming */\nfun unboundedKnapsackDPComp(\n    wgt: IntArray,\n    _val: IntArray,\n    cap: Int\n): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = IntArray(cap + 1)\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item 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
    ### Unbounded knapsack: space-optimized DP ###\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(cap + 1, 0)\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for c in 1...(cap + 1)\n      if wgt[i -1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c]\n      else\n        # The larger value between not selecting and selecting item 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":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452-coin-change-problem","level":2,"title":"14.5.2   Coin Change Problem","text":"

    The knapsack problem represents a large class of dynamic programming problems and has many variants, such as the coin change problem.

    Question

    Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\). Each type of coin can be selected multiple times. What is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return \\(-1\\). An example is shown in Figure 14-24.

    Figure 14-24   Example data for coin change problem

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach_1","level":3,"title":"1.   Dynamic Programming Approach","text":"

    The coin change problem can be viewed as a special case of the unbounded knapsack problem, with the following connections and differences.

    • The two problems can be converted to each other: \"item\" corresponds to \"coin\", \"item weight\" corresponds to \"coin denomination\", and \"knapsack capacity\" corresponds to \"target amount\".
    • The optimization goals are opposite: the unbounded knapsack problem aims to maximize item value, while the coin change problem aims to minimize the number of coins.
    • The unbounded knapsack problem seeks solutions \"not exceeding\" the knapsack capacity, while the coin change problem seeks solutions that \"exactly\" make up the target amount.

    Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

    State \\([i, a]\\) corresponds to the subproblem: the minimum number of coins among the first \\(i\\) types of coins that can make up amount \\(a\\), denoted as \\(dp[i, a]\\).

    The two-dimensional \\(dp\\) table has size \\((n+1) \\times (amt+1)\\).

    Step 2: Identify the optimal substructure, and then derive the state transition equation

    This problem differs from the unbounded knapsack problem in the following two aspects regarding the state transition equation.

    • This problem seeks the minimum value, so the operator \\(\\max()\\) needs to be changed to \\(\\min()\\).
    • The optimization target is the number of coins rather than item value, so when a coin is selected, simply add \\(1\\).
    \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

    Step 3: Determine boundary conditions and state transition order

    When the target amount is \\(0\\), the minimum number of coins needed to make it up is \\(0\\), so all \\(dp[i, 0]\\) in the first column equal \\(0\\).

    When there are no coins, it is impossible to make up any amount \\(> 0\\), which is an invalid solution. To enable the \\(\\min()\\) function in the state transition equation to identify and filter out invalid solutions, we consider using \\(+ \\infty\\) to represent them, i.e., set all \\(dp[0, a]\\) in the first row to \\(+ \\infty\\).

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation_1","level":3,"title":"2.   Code Implementation","text":"

    Most programming languages do not provide a \\(+ \\infty\\) variable, and can only use the maximum value of integer type int as a substitute. However, this can lead to integer overflow: the \\(+ 1\\) operation in the state transition equation may cause overflow.

    For this reason, we use the number \\(amt + 1\\) to represent invalid solutions, because the maximum number of coins needed to make up \\(amt\\) is at most \\(amt\\). Before returning, check whether \\(dp[n, amt]\\) equals \\(amt + 1\\); if so, return \\(-1\\), indicating that the target amount cannot be made up. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
    def coin_change_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Dynamic programming\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Initialize dp table\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # State transition: first row and first column\n    for a in range(1, amt + 1):\n        dp[0][a] = MAX\n    # State transition: rest of the rows and columns\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint coinChangeDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint coinChangeDP(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][amt + 1];\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint CoinChangeDP(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, amt + 1];\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0, a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunc coinChangeDP(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // State transition: first row and first column\n    for a := 1; a <= amt; a++ {\n        dp[0][a] = max\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunc coinChangeDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // State transition: first row and first column\n    for a in 1 ... amt {\n        dp[0][a] = MAX\n    }\n    // State transition: rest of the rows and columns\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunction coinChangeDP(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunction coinChangeDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint coinChangeDP(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // State transition: first row and first column\n  for (int a = 1; a <= amt; a++) {\n    dp[0][a] = MAX;\n  }\n  // State transition: rest of the rows and columns\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfn coin_change_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Initialize dp table\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // State transition: first row and first column\n    for a in 1..=amt {\n        dp[0][a] = max;\n    }\n    // State transition: rest of the rows and columns\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Initialize dp table\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    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
    coin_change.kt
    /* Coin change: Dynamic programming */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // State transition: first row and first column\n    for (a in 1..amt) {\n        dp[0][a] = MAX\n    }\n    // State transition: rest of the rows and columns\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    ### Coin change: dynamic programming ###\ndef coin_change_dp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # State transition: first row and first column\n  (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n  # State transition: rest of the rows and columns\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # The smaller value between not selecting and selecting coin 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

    Figure 14-25 shows the dynamic programming process for coin change, which is very similar to the unbounded knapsack problem.

    <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

    Figure 14-25   Dynamic programming process for coin change problem

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization_1","level":3,"title":"3.   Space Optimization","text":"

    The space optimization for the coin change problem is handled in the same way as the unbounded knapsack problem:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
    def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Space-optimized dynamic programming\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Initialize dp table\n    dp = [MAX] * (amt + 1)\n    dp[0] = 0\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            else:\n                # The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Initialize dp table\n    vector<int> dp(amt + 1, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    Arrays.fill(dp, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint CoinChangeDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    Array.Fill(dp, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Dynamic programming */\nfunc coinChangeDPComp(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Initialize dp table\n    dp := make([]int, amt+1)\n    for i := 1; i <= amt; i++ {\n        dp[i] = max\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in forward order\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Initialize dp table\n    var dp = Array(repeating: MAX, count: amt + 1)\n    dp[0] = 0\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nfunction coinChangeDPComp(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nfunction coinChangeDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Initialize dp table\n  List<int> dp = List.filled(amt + 1, MAX);\n  dp[0] = 0;\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[a] = dp[a];\n      } else {\n        // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nfn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Initialize dp table\n    let mut dp = vec![0; amt + 1];\n    dp.fill(max);\n    dp[0] = 0;\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    /* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Initialize dp table\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    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin 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    // Free memory\n    free(dp);\n    return res;\n}\n
    coin_change.kt
    /* Coin change: Space-optimized dynamic programming */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Initialize dp table\n    val dp = IntArray(amt + 1)\n    dp.fill(MAX)\n    dp[0] = 0\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin 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
    ### Coin change: space-optimized DP ###\ndef coin_change_dp_comp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Initialize dp table\n  dp = Array.new(amt + 1, _MAX)\n  dp[0] = 0\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[a] = dp[a]\n      else\n        # The smaller value between not selecting and selecting coin 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":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-coin-change-problem-ii","level":2,"title":"14.5.3   Coin Change Problem II","text":"

    Question

    Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\). Each type of coin can be selected multiple times. What is the number of coin combinations that can make up the target amount? An example is shown in Figure 14-26.

    Figure 14-26   Example data for coin change problem II

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach_2","level":3,"title":"1.   Dynamic Programming Approach","text":"

    Compared to the previous problem, this problem's goal is to find the number of combinations, so the subproblem becomes: the number of combinations among the first \\(i\\) types of coins that can make up amount \\(a\\). The \\(dp\\) table remains a two-dimensional matrix of size \\((n+1) \\times (amt + 1)\\).

    The number of combinations for the current state equals the sum of the combinations from not selecting the current coin and selecting the current coin. The state transition equation is:

    \\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

    When the target amount is \\(0\\), no coins need to be selected to make up the target amount, so all \\(dp[i, 0]\\) in the first column should be initialized to \\(1\\). When there are no coins, it is impossible to make up any amount \\(>0\\), so all \\(dp[0, a]\\) in the first row equal \\(0\\).

    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation_2","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
    def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change II: Dynamic programming\"\"\"\n    n = len(coins)\n    # Initialize dp table\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # Initialize first column\n    for i in range(n + 1):\n        dp[i][0] = 1\n    # State transition\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint coinChangeIIDP(int[] coins, int amt) {\n    int n = coins.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][amt + 1];\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint CoinChangeIIDP(int[] coins, int amt) {\n    int n = coins.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, amt + 1];\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i, 0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfunc coinChangeIIDP(coins []int, amt int) int {\n    n := len(coins)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // Initialize first column\n    for i := 0; i <= n; i++ {\n        dp[i][0] = 1\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // Initialize first column\n    for i in 0 ... n {\n        dp[i][0] = 1\n    }\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfunction coinChangeIIDP(coins, amt) {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Initialize first column\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfunction coinChangeIIDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Initialize first column\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint coinChangeIIDP(List<int> coins, int amt) {\n  int n = coins.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // Initialize first column\n  for (int i = 0; i <= n; i++) {\n    dp[i][0] = 1;\n  }\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nfn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // Initialize first column\n    for i in 0..=n {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Dynamic programming */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Initialize dp table\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    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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    // Free memory\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
    /* Coin change II: Dynamic programming */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // Initialize first column\n    for (i in 0..n) {\n        dp[i][0] = 1\n    }\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    ### Coin change II: dynamic programming ###\ndef coin_change_ii_dp(coins, amt)\n  n = coins.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # Initialize first column\n  (0...(n + 1)).each { |i| dp[i][0] = 1 }\n  # State transition\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # Sum of the two options: not selecting and selecting coin 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":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization_2","level":3,"title":"3.   Space Optimization","text":"

    The space optimization is handled in the same way, just delete the coin dimension:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
    def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change II: Space-optimized dynamic programming\"\"\"\n    n = len(coins)\n    # Initialize dp table\n    dp = [0] * (amt + 1)\n    dp[0] = 1\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            else:\n                # Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n    return dp[amt]\n
    coin_change_ii.cpp
    /* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Initialize dp table\n    vector<int> dp(amt + 1, 0);\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.java
    /* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.cs
    /* Coin change II: Space-optimized dynamic programming */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.go
    /* Coin change II: Space-optimized dynamic programming */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n    n := len(coins)\n    // Initialize dp table\n    dp := make([]int, amt+1)\n    dp[0] = 1\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in forward order\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a-coins[i-1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
    coin_change_ii.swift
    /* Coin change II: Space-optimized dynamic programming */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: amt + 1)\n    dp[0] = 1\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
    coin_change_ii.js
    /* Coin change II: Space-optimized dynamic programming */\nfunction coinChangeIIDPComp(coins, amt) {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.ts
    /* Coin change II: Space-optimized dynamic programming */\nfunction coinChangeIIDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
    coin_change_ii.dart
    /* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  // Initialize dp table\n  List<int> dp = List.filled(amt + 1, 0);\n  dp[0] = 1;\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[a] = dp[a];\n      } else {\n        // Sum of the two options: not selecting and selecting coin i\n        dp[a] = dp[a] + dp[a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[amt];\n}\n
    coin_change_ii.rs
    /* Coin change II: Space-optimized dynamic programming */\nfn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Initialize dp table\n    let mut dp = vec![0; amt + 1];\n    dp[0] = 1;\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin 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
    /* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Initialize dp table\n    int *dp = calloc(amt + 1, sizeof(int));\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[amt];\n    // Free memory\n    free(dp);\n    return res;\n}\n
    coin_change_ii.kt
    /* Coin change II: Space-optimized dynamic programming */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Initialize dp table\n    val dp = IntArray(amt + 1)\n    dp[0] = 1\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
    coin_change_ii.rb
    ### Coin change II: space-optimized DP ###\ndef coin_change_ii_dp_comp(coins, amt)\n  n = coins.length\n  # Initialize dp table\n  dp = Array.new(amt + 1, 0)\n  dp[0] = 1\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[a] = dp[a]\n      else\n        # Sum of the two options: not selecting and selecting coin i\n        dp[a] = dp[a] + dp[a - coins[i - 1]]\n      end\n    end\n  end\n  dp[amt]\nend\n
    ","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"Chapter 9.   Graph","text":"

    Abstract

    In the journey of life, we are like nodes, connected by countless invisible edges.

    Each encounter and parting leaves a unique mark on this vast network graph.

    ","path":["Chapter 9. Graph","Chapter 9.   Graph"],"tags":[]},{"location":"chapter_graph/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 9.1   Graph
    • 9.2   Basic Operations on Graphs
    • 9.3   Graph Traversal
    • 9.4   Summary
    ","path":["Chapter 9. Graph","Chapter 9.   Graph"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   Graph","text":"

    A graph is a nonlinear data structure consisting of vertices and edges. We can abstractly represent a graph \\(G\\) as a set of vertices \\(V\\) and a set of edges \\(E\\). The following example shows a graph containing 5 vertices and 7 edges.

    \\[ \\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} \\]

    If we view vertices as nodes and edges as references (pointers) connecting them, we can regard a graph as an extension of the linked list data structure. As shown in Figure 9-1, compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex.

    Figure 9-1   Relationships among linked lists, trees, and graphs

    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#911-common-types-and-terminology-of-graphs","level":2,"title":"9.1.1   Common Types and Terminology of Graphs","text":"

    Graphs can be divided into undirected graphs and directed graphs based on whether edges have direction, as shown in Figure 9-2.

    • In undirected graphs, edges represent a \"bidirectional\" connection between two vertices, such as friendships on WeChat or QQ.
    • In directed graphs, edges have directionality, meaning edges \\(A \\rightarrow B\\) and \\(A \\leftarrow B\\) are independent of each other, such as following and follower relationships on Weibo or TikTok.

    Figure 9-2   Directed and undirected graphs

    Graphs can be divided into connected graphs and disconnected graphs based on whether all vertices are connected, as shown in Figure 9-3.

    • For connected graphs, starting from any vertex, all other vertices can be reached.
    • For disconnected graphs, starting from a certain vertex, at least one vertex cannot be reached.

    Figure 9-3   Connected and disconnected graphs

    We can also add a \"weight\" variable to edges, resulting in weighted graphs as shown in Figure 9-4. For example, in mobile games like \"Honor of Kings\", the system calculates the \"intimacy\" between players based on how long they have played together, and such intimacy networks can be represented using weighted graphs.

    Figure 9-4   Weighted and unweighted graphs

    Graph data structures include the following commonly used terms.

    • Adjacency: When two vertices are connected by an edge, these two vertices are said to be \"adjacent\". In Figure 9-4, the adjacent vertices of vertex 1 are vertices 2, 3, and 5.
    • Path: The sequence of edges from vertex A to vertex B is called a \"path\" from A to B. In Figure 9-4, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4.
    • Degree: The number of edges a vertex has. For directed graphs, in-degree indicates how many edges point to the vertex, and out-degree indicates how many edges leave the vertex.
    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#912-representation-of-graphs","level":2,"title":"9.1.2   Representation of Graphs","text":"

    Common representations of graphs include \"adjacency matrices\" and \"adjacency lists\". The following uses undirected graphs as examples.

    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#1-adjacency-matrix","level":3,"title":"1.   Adjacency Matrix","text":"

    Given a graph with \\(n\\) vertices, an adjacency matrix uses an \\(n \\times n\\) matrix to represent the graph, where each row (column) represents a vertex, and matrix elements represent edges, using \\(1\\) or \\(0\\) to indicate whether an edge exists between two vertices.

    As shown in Figure 9-5, let the adjacency matrix be \\(M\\) and the vertex list be \\(V\\). Then matrix element \\(M[i, j] = 1\\) indicates that an edge exists between vertex \\(V[i]\\) and vertex \\(V[j]\\), whereas \\(M[i, j] = 0\\) indicates no edge between the two vertices.

    Figure 9-5   Adjacency matrix representation of a graph

    Adjacency matrices have the following properties.

    • In simple graphs, vertices cannot connect to themselves, so the elements on the main diagonal of the adjacency matrix are meaningless.
    • For undirected graphs, edges in both directions are equivalent, so the adjacency matrix is symmetric about the main diagonal.
    • Replacing the \\(1\\) and \\(0\\) entries in the adjacency matrix with weights allows it to represent weighted graphs.

    When using adjacency matrices to represent graphs, we can directly access matrix elements to obtain edges, resulting in highly efficient addition, deletion, lookup, and modification operations, all with a time complexity of \\(O(1)\\). However, the space complexity of the matrix is \\(O(n^2)\\), which consumes significant memory.

    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#2-adjacency-list","level":3,"title":"2.   Adjacency List","text":"

    An adjacency list uses \\(n\\) linked lists to represent a graph, with linked list nodes representing vertices. The \\(i\\)-th linked list corresponds to vertex \\(i\\) and stores all adjacent vertices of that vertex (vertices connected to that vertex). Figure 9-6 shows an example of a graph stored using an adjacency list.

    Figure 9-6   Adjacency list representation of a graph

    Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than \\(n^2\\), making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so it is less time-efficient than an adjacency matrix.

    As shown in Figure 9-6, the structure of adjacency lists is very similar to separate chaining in hash tables, so we can use similar methods to improve efficiency. For example, when a linked list becomes long, it can be converted into an AVL tree or red-black tree, improving the time complexity from \\(O(n)\\) to \\(O(\\log n)\\); it can also be converted into a hash table, reducing the time complexity to \\(O(1)\\).

    ","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#913-common-applications-of-graphs","level":2,"title":"9.1.3   Common Applications of Graphs","text":"

    As shown in Table 9-1, many real-world systems can be modeled using graphs, and corresponding problems can be reduced to graph computation problems.

    Table 9-1   Common graphs in real life

    Vertices Edges Graph Computation Problem Social network Users Friend relationships Potential friend recommendation Subway lines Stations Connectivity between stations Shortest route recommendation Solar system Celestial bodies Gravitational forces between celestial bodies Planetary orbit calculation","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   Basic Operations on Graphs","text":"

    Basic operations on graphs can be divided into operations on \"edges\" and operations on \"vertices\". Their implementations differ depending on whether the graph is represented as an \"adjacency matrix\" or an \"adjacency list\".

    ","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#921-implementation-based-on-adjacency-matrix","level":2,"title":"9.2.1   Implementation Based on Adjacency Matrix","text":"

    Given an undirected graph with \\(n\\) vertices, the various operations are implemented as shown in Figure 9-7.

    • Adding or removing an edge: Directly modify the specified edge in the adjacency matrix, using \\(O(1)\\) time. Since it is an undirected graph, both directions of the edge need to be updated simultaneously.
    • Adding a vertex: Add a row and a column at the end of the adjacency matrix and fill them all with \\(0\\)s, using \\(O(n)\\) time.
    • Removing a vertex: Delete a row and a column in the adjacency matrix. The worst case occurs when removing the first row and column, requiring \\((n-1)^2\\) elements to be \"moved up and to the left\", thus using \\(O(n^2)\\) time.
    • Initialization: Given \\(n\\) vertices, initialize a vertex list vertices of length \\(n\\), using \\(O(n)\\) time; initialize an adjacency matrix adjMat of size \\(n \\times n\\), using \\(O(n^2)\\) time.
    <1><2><3><4><5>

    Figure 9-7   Initialization, adding and removing edges, adding and removing vertices in adjacency matrix

    The following is the implementation code for graphs represented using an adjacency matrix:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py
    class GraphAdjMat:\n    \"\"\"Undirected graph class based on adjacency matrix\"\"\"\n\n    def __init__(self, vertices: list[int], edges: list[list[int]]):\n        \"\"\"Constructor\"\"\"\n        # Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n        self.vertices: list[int] = []\n        # Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n        self.adj_mat: list[list[int]] = []\n        # Add vertices\n        for val in vertices:\n            self.add_vertex(val)\n        # Add edges\n        # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for e in edges:\n            self.add_edge(e[0], e[1])\n\n    def size(self) -> int:\n        \"\"\"Get the number of vertices\"\"\"\n        return len(self.vertices)\n\n    def add_vertex(self, val: int):\n        \"\"\"Add vertex\"\"\"\n        n = self.size()\n        # Add the value of the new vertex to the vertex list\n        self.vertices.append(val)\n        # Add a row to the adjacency matrix\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # Add a column to the adjacency matrix\n        for row in self.adj_mat:\n            row.append(0)\n\n    def remove_vertex(self, index: int):\n        \"\"\"Remove vertex\"\"\"\n        if index >= self.size():\n            raise IndexError()\n        # Remove the vertex at index from the vertex list\n        self.vertices.pop(index)\n        # Remove the row at index from the adjacency matrix\n        self.adj_mat.pop(index)\n        # Remove the column at index from the adjacency matrix\n        for row in self.adj_mat:\n            row.pop(index)\n\n    def add_edge(self, i: int, j: int):\n        \"\"\"Add edge\"\"\"\n        # Parameters i, j correspond to the vertices element indices\n        # Handle index out of bounds and equality\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (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        \"\"\"Remove edge\"\"\"\n        # Parameters i, j correspond to the vertices element indices\n        # Handle index out of bounds and equality\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        \"\"\"Print adjacency matrix\"\"\"\n        print(\"Vertex list =\", self.vertices)\n        print(\"Adjacency matrix =\")\n        print_matrix(self.adj_mat)\n
    graph_adjacency_matrix.cpp
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vector<int> vertices;       // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    vector<vector<int>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n  public:\n    /* Constructor */\n    GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n        // Add vertex\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const vector<int> &edge : edges) {\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int size() const {\n        return vertices.size();\n    }\n\n    /* Add vertex */\n    void addVertex(int val) {\n        int n = size();\n        // Add the value of the new vertex to the vertex list\n        vertices.push_back(val);\n        // Add a row to the adjacency matrix\n        adjMat.emplace_back(vector<int>(n, 0));\n        // Add a column to the adjacency matrix\n        for (vector<int> &row : adjMat) {\n            row.push_back(0);\n        }\n    }\n\n    /* Remove vertex */\n    void removeVertex(int index) {\n        if (index >= size()) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        // Remove the vertex at index from the vertex list\n        vertices.erase(vertices.begin() + index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.erase(adjMat.begin() + index);\n        // Remove the column at index from the adjacency matrix\n        for (vector<int> &row : adjMat) {\n            row.erase(row.begin() + index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    void addEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    void removeEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    void print() {\n        cout << \"Vertex list = \";\n        printVector(vertices);\n        cout << \"Adjacency matrix =\" << endl;\n        printVectorMatrix(adjMat);\n    }\n};\n
    graph_adjacency_matrix.java
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    List<Integer> vertices; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    List<List<Integer>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = new ArrayList<>();\n        this.adjMat = new ArrayList<>();\n        // Add vertex\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (int[] e : edges) {\n            addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    public int size() {\n        return vertices.size();\n    }\n\n    /* Add vertex */\n    public void addVertex(int val) {\n        int n = size();\n        // Add the value of the new vertex to the vertex list\n        vertices.add(val);\n        // Add a row to the adjacency matrix\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        // Add a column to the adjacency matrix\n        for (List<Integer> row : adjMat) {\n            row.add(0);\n        }\n    }\n\n    /* Remove vertex */\n    public void removeVertex(int index) {\n        if (index >= size())\n            throw new IndexOutOfBoundsException();\n        // Remove the vertex at index from the vertex list\n        vertices.remove(index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.remove(index);\n        // Remove the column at index from the adjacency matrix\n        for (List<Integer> row : adjMat) {\n            row.remove(index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void addEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat.get(i).set(j, 1);\n        adjMat.get(j).set(i, 1);\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void removeEdge(int i, int j) {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    public void print() {\n        System.out.print(\"Vertex list = \");\n        System.out.println(vertices);\n        System.out.println(\"Adjacency matrix =\");\n        PrintUtil.printMatrix(adjMat);\n    }\n}\n
    graph_adjacency_matrix.cs
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    List<int> vertices;     // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    List<List<int>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        foreach (int val in vertices) {\n            AddVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        foreach (int[] e in edges) {\n            AddEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int Size() {\n        return vertices.Count;\n    }\n\n    /* Add vertex */\n    public void AddVertex(int val) {\n        int n = Size();\n        // Add the value of the new vertex to the vertex list\n        vertices.Add(val);\n        // Add a row to the adjacency matrix\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // Add a column to the adjacency matrix\n        foreach (List<int> row in adjMat) {\n            row.Add(0);\n        }\n    }\n\n    /* Remove vertex */\n    public void RemoveVertex(int index) {\n        if (index >= Size())\n            throw new IndexOutOfRangeException();\n        // Remove the vertex at index from the vertex list\n        vertices.RemoveAt(index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.RemoveAt(index);\n        // Remove the column at index from the adjacency matrix\n        foreach (List<int> row in adjMat) {\n            row.RemoveAt(index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void AddEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void RemoveEdge(int i, int j) {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    public void Print() {\n        Console.Write(\"Vertex list = \");\n        PrintUtil.PrintList(vertices);\n        Console.WriteLine(\"Adjacency matrix =\");\n        PrintUtil.PrintMatrix(adjMat);\n    }\n}\n
    graph_adjacency_matrix.go
    /* Undirected graph class based on adjacency matrix */\ntype graphAdjMat struct {\n    // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    vertices []int\n    // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    adjMat [][]int\n}\n\n/* Constructor */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n    // Add vertex\n    n := len(vertices)\n    adjMat := make([][]int, n)\n    for i := range adjMat {\n        adjMat[i] = make([]int, n)\n    }\n    // Initialize graph\n    g := &graphAdjMat{\n        vertices: vertices,\n        adjMat:   adjMat,\n    }\n    // Add edge\n    // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    for i := range edges {\n        g.addEdge(edges[i][0], edges[i][1])\n    }\n    return g\n}\n\n/* Get the number of vertices */\nfunc (g *graphAdjMat) size() int {\n    return len(g.vertices)\n}\n\n/* Add vertex */\nfunc (g *graphAdjMat) addVertex(val int) {\n    n := g.size()\n    // Add the value of the new vertex to the vertex list\n    g.vertices = append(g.vertices, val)\n    // Add a row to the adjacency matrix\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // Add a column to the adjacency matrix\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i], 0)\n    }\n}\n\n/* Remove vertex */\nfunc (g *graphAdjMat) removeVertex(index int) {\n    if index >= g.size() {\n        return\n    }\n    // Remove the vertex at index from the vertex list\n    g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n    // Remove the row at index from the adjacency matrix\n    g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n    // Remove the column at index from the adjacency matrix\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n    }\n}\n\n/* Add edge */\n// Parameters i, j correspond to the vertices element indices\nfunc (g *graphAdjMat) addEdge(i, j int) {\n    // Handle index out of bounds and equality\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    // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    g.adjMat[i][j] = 1\n    g.adjMat[j][i] = 1\n}\n\n/* Remove edge */\n// Parameters i, j correspond to the vertices element indices\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n    // Handle index out of bounds and equality\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/* Print adjacency matrix */\nfunc (g *graphAdjMat) print() {\n    fmt.Printf(\"\\tVertex list = %v\\n\", g.vertices)\n    fmt.Printf(\"\\tAdjacency matrix = \\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
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    private var vertices: [Int] // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    private var adjMat: [[Int]] // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    init(vertices: [Int], edges: [[Int]]) {\n        self.vertices = []\n        adjMat = []\n        // Add vertex\n        for val in vertices {\n            addVertex(val: val)\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for e in edges {\n            addEdge(i: e[0], j: e[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    func size() -> Int {\n        vertices.count\n    }\n\n    /* Add vertex */\n    func addVertex(val: Int) {\n        let n = size()\n        // Add the value of the new vertex to the vertex list\n        vertices.append(val)\n        // Add a row to the adjacency matrix\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // Add a column to the adjacency matrix\n        for i in adjMat.indices {\n            adjMat[i].append(0)\n        }\n    }\n\n    /* Remove vertex */\n    func removeVertex(index: Int) {\n        if index >= size() {\n            fatalError(\"Out of bounds\")\n        }\n        // Remove the vertex at index from the vertex list\n        vertices.remove(at: index)\n        // Remove the row at index from the adjacency matrix\n        adjMat.remove(at: index)\n        // Remove the column at index from the adjacency matrix\n        for i in adjMat.indices {\n            adjMat[i].remove(at: index)\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    func addEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Out of bounds\")\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    func removeEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Out of bounds\")\n        }\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Print adjacency matrix */\n    func print() {\n        Swift.print(\"Vertex list = \", terminator: \"\")\n        Swift.print(vertices)\n        Swift.print(\"Adjacency matrix =\")\n        PrintUtil.printMatrix(matrix: adjMat)\n    }\n}\n
    graph_adjacency_matrix.js
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vertices; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    constructor(vertices, edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size() {\n        return this.vertices.length;\n    }\n\n    /* Add vertex */\n    addVertex(val) {\n        const n = this.size();\n        // Add the value of the new vertex to the vertex list\n        this.vertices.push(val);\n        // Add a row to the adjacency matrix\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Add a column to the adjacency matrix\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    removeVertex(index) {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Remove the vertex at index from the vertex list\n        this.vertices.splice(index, 1);\n\n        // Remove the row at index from the adjacency matrix\n        this.adjMat.splice(index, 1);\n        // Remove the column at index from the adjacency matrix\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    addEdge(i, j) {\n        // Handle index out of bounds and equality\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        // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    removeEdge(i, j) {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    print() {\n        console.log('Vertex list = ', this.vertices);\n        console.log('Adjacency matrix =', this.adjMat);\n    }\n}\n
    graph_adjacency_matrix.ts
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vertices: number[]; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    adjMat: number[][]; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    constructor(vertices: number[], edges: number[][]) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size(): number {\n        return this.vertices.length;\n    }\n\n    /* Add vertex */\n    addVertex(val: number): void {\n        const n: number = this.size();\n        // Add the value of the new vertex to the vertex list\n        this.vertices.push(val);\n        // Add a row to the adjacency matrix\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Add a column to the adjacency matrix\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    removeVertex(index: number): void {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Remove the vertex at index from the vertex list\n        this.vertices.splice(index, 1);\n\n        // Remove the row at index from the adjacency matrix\n        this.adjMat.splice(index, 1);\n        // Remove the column at index from the adjacency matrix\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    addEdge(i: number, j: number): void {\n        // Handle index out of bounds and equality\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        // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    removeEdge(i: number, j: number): void {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    print(): void {\n        console.log('Vertex list = ', this.vertices);\n        console.log('Adjacency matrix =', this.adjMat);\n    }\n}\n
    graph_adjacency_matrix.dart
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n  List<int> vertices = []; // Vertex elements, elements represent \"vertex values\", indices represent \"vertex indices\"\n  List<List<int>> adjMat = []; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n  /* Constructor */\n  GraphAdjMat(List<int> vertices, List<List<int>> edges) {\n    this.vertices = [];\n    this.adjMat = [];\n    // Add vertex\n    for (int val in vertices) {\n      addVertex(val);\n    }\n    // Add edge\n    // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    for (List<int> e in edges) {\n      addEdge(e[0], e[1]);\n    }\n  }\n\n  /* Get the number of vertices */\n  int size() {\n    return vertices.length;\n  }\n\n  /* Add vertex */\n  void addVertex(int val) {\n    int n = size();\n    // Add the value of the new vertex to the vertex list\n    vertices.add(val);\n    // Add a row to the adjacency matrix\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // Add a column to the adjacency matrix\n    for (List<int> row in adjMat) {\n      row.add(0);\n    }\n  }\n\n  /* Remove vertex */\n  void removeVertex(int index) {\n    if (index >= size()) {\n      throw IndexError;\n    }\n    // Remove the vertex at index from the vertex list\n    vertices.removeAt(index);\n    // Remove the row at index from the adjacency matrix\n    adjMat.removeAt(index);\n    // Remove the column at index from the adjacency matrix\n    for (List<int> row in adjMat) {\n      row.removeAt(index);\n    }\n  }\n\n  /* Add edge */\n  // Parameters i, j correspond to the vertices element indices\n  void addEdge(int i, int j) {\n    // Handle index out of bounds and equality\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    adjMat[i][j] = 1;\n    adjMat[j][i] = 1;\n  }\n\n  /* Remove edge */\n  // Parameters i, j correspond to the vertices element indices\n  void removeEdge(int i, int j) {\n    // Handle index out of bounds and equality\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  /* Print adjacency matrix */\n  void printAdjMat() {\n    print(\"Vertex list = $vertices\");\n    print(\"Adjacency matrix = \");\n    printMatrix(adjMat);\n  }\n}\n
    graph_adjacency_matrix.rs
    /* Undirected graph type based on adjacency matrix */\npub struct GraphAdjMat {\n    // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    pub vertices: Vec<i32>,\n    // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    pub adj_mat: Vec<Vec<i32>>,\n}\n\nimpl GraphAdjMat {\n    /* Constructor */\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        // Add vertex\n        for val in vertices {\n            graph.add_vertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for edge in edges {\n            graph.add_edge(edge[0], edge[1])\n        }\n\n        graph\n    }\n\n    /* Get the number of vertices */\n    pub fn size(&self) -> usize {\n        self.vertices.len()\n    }\n\n    /* Add vertex */\n    pub fn add_vertex(&mut self, val: i32) {\n        let n = self.size();\n        // Add the value of the new vertex to the vertex list\n        self.vertices.push(val);\n        // Add a row to the adjacency matrix\n        self.adj_mat.push(vec![0; n]);\n        // Add a column to the adjacency matrix\n        for row in self.adj_mat.iter_mut() {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    pub fn remove_vertex(&mut self, index: usize) {\n        if index >= self.size() {\n            panic!(\"index error\")\n        }\n        // Remove the vertex at index from the vertex list\n        self.vertices.remove(index);\n        // Remove the row at index from the adjacency matrix\n        self.adj_mat.remove(index);\n        // Remove the column at index from the adjacency matrix\n        for row in self.adj_mat.iter_mut() {\n            row.remove(index);\n        }\n    }\n\n    /* Add edge */\n    pub fn add_edge(&mut self, i: usize, j: usize) {\n        // Parameters i, j correspond to the vertices element indices\n        // Handle index out of bounds and equality\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        self.adj_mat[i][j] = 1;\n        self.adj_mat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    pub fn remove_edge(&mut self, i: usize, j: usize) {\n        // Parameters i, j correspond to the vertices element indices\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    pub fn print(&self) {\n        println!(\"Vertex list = {:?}\", self.vertices);\n        println!(\"Adjacency matrix =\");\n        println!(\"[\");\n        for row in &self.adj_mat {\n            println!(\"  {:?},\", row);\n        }\n        println!(\"]\")\n    }\n}\n
    graph_adjacency_matrix.c
    /* Undirected graph structure based on adjacency matrix */\ntypedef struct {\n    int vertices[MAX_SIZE];\n    int adjMat[MAX_SIZE][MAX_SIZE];\n    int size;\n} GraphAdjMat;\n\n/* Constructor */\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/* Destructor */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n    free(graph);\n}\n\n/* Add vertex */\nvoid addVertex(GraphAdjMat *graph, int val) {\n    if (graph->size == MAX_SIZE) {\n        fprintf(stderr, \"Graph vertex count has reached maximum\\n\");\n        return;\n    }\n    // Add nth vertex and zero nth row and column\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/* Remove vertex */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n    if (index < 0 || index >= graph->size) {\n        fprintf(stderr, \"Vertex index out of bounds\\n\");\n        return;\n    }\n    // Remove the vertex at index from the vertex list\n    for (int i = index; i < graph->size - 1; i++) {\n        graph->vertices[i] = graph->vertices[i + 1];\n    }\n    // Remove the row at index from the adjacency matrix\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    // Remove the column at index from the adjacency matrix\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/* Add edge */\n// Parameters i, j correspond to the vertices element indices\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, \"Edge index out of bounds or equal\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 1;\n    graph->adjMat[j][i] = 1;\n}\n\n/* Remove edge */\n// Parameters i, j correspond to the vertices element indices\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, \"Edge index out of bounds or equal\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 0;\n    graph->adjMat[j][i] = 0;\n}\n\n/* Print adjacency matrix */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n    printf(\"Vertex list = \");\n    printArray(graph->vertices, graph->size);\n    printf(\"Adjacency matrix =\\n\");\n    for (int i = 0; i < graph->size; i++) {\n        printArray(graph->adjMat[i], graph->size);\n    }\n}\n
    graph_adjacency_matrix.kt
    /* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat(vertices: IntArray, edges: Array<IntArray>) {\n    val vertices = mutableListOf<Int>() // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    val adjMat = mutableListOf<MutableList<Int>>() // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    init {\n        // Add vertex\n        for (vertex in vertices) {\n            addVertex(vertex)\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (edge in edges) {\n            addEdge(edge[0], edge[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    fun size(): Int {\n        return vertices.size\n    }\n\n    /* Add vertex */\n    fun addVertex(_val: Int) {\n        val n = size()\n        // Add the value of the new vertex to the vertex list\n        vertices.add(_val)\n        // Add a row to the adjacency matrix\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // Add a column to the adjacency matrix\n        for (row in adjMat) {\n            row.add(0)\n        }\n    }\n\n    /* Remove vertex */\n    fun removeVertex(index: Int) {\n        if (index >= size())\n            throw IndexOutOfBoundsException()\n        // Remove the vertex at index from the vertex list\n        vertices.removeAt(index)\n        // Remove the row at index from the adjacency matrix\n        adjMat.removeAt(index)\n        // Remove the column at index from the adjacency matrix\n        for (row in adjMat) {\n            row.removeAt(index)\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    fun addEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    fun removeEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\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    /* Print adjacency matrix */\n    fun print() {\n        print(\"Vertex list = \")\n        println(vertices)\n        println(\"Adjacency matrix =\")\n        printMatrix(adjMat)\n    }\n}\n
    graph_adjacency_matrix.rb
    ### Undirected graph class based on adjacency matrix ###\nclass GraphAdjMat\n  def initialize(vertices, edges)\n    ### Constructor ###\n    # Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    @vertices = []\n    # Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    @adj_mat = []\n    # Add vertex\n    vertices.each { |val| add_vertex(val) }\n    # Add edge\n    # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    edges.each { |e| add_edge(e[0], e[1]) }\n  end\n\n  ### Get number of vertices ###\n  def size\n    @vertices.length\n  end\n\n  ### Add vertex ###\n  def add_vertex(val)\n    n = size\n    # Add the value of the new vertex to the vertex list\n    @vertices << val\n    # Add a row to the adjacency matrix\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # Add a column to the adjacency matrix\n    @adj_mat.each { |row| row << 0 }\n  end\n\n  ### Delete vertex ###\n  def remove_vertex(index)\n    raise IndexError if index >= size\n\n    # Remove the vertex at index from the vertex list\n    @vertices.delete_at(index)\n    # Remove the row at index from the adjacency matrix\n    @adj_mat.delete_at(index)\n    # Remove the column at index from the adjacency matrix\n    @adj_mat.each { |row| row.delete_at(index) }\n  end\n\n  ### Add edge ###\n  def add_edge(i, j)\n    # Parameters i, j correspond to the vertices element indices\n    # Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    @adj_mat[i][j] = 1\n    @adj_mat[j][i] = 1\n  end\n\n  ### Delete edge ###\n  def remove_edge(i, j)\n    # Parameters i, j correspond to the vertices element indices\n    # Handle index out of bounds and equality\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  ### Print adjacency matrix ###\n  def __print__\n    puts \"Vertex list = #{@vertices}\"\n    puts 'Adjacency matrix ='\n    print_matrix(@adj_mat)\n  end\nend\n
    ","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#922-implementation-based-on-adjacency-list","level":2,"title":"9.2.2   Implementation Based on Adjacency List","text":"

    Given an undirected graph with a total of \\(n\\) vertices and \\(m\\) edges, the various operations can be implemented as shown in Figure 9-8.

    • Adding an edge: Add the edge at the end of the corresponding vertex's linked list, using \\(O(1)\\) time. Since it is an undirected graph, edges in both directions need to be added simultaneously.
    • Removing an edge: Find and remove the specified edge in the corresponding vertex's linked list, using \\(O(m)\\) time. In an undirected graph, edges in both directions need to be removed simultaneously.
    • Adding a vertex: Add a linked list to the adjacency list, with the new vertex as the head node, using \\(O(1)\\) time.
    • Removing a vertex: Traverse the entire adjacency list and remove all edges containing the specified vertex, using \\(O(n + m)\\) time.
    • Initialization: Create \\(n\\) vertices and \\(2m\\) edges in the adjacency list, using \\(O(n + m)\\) time.
    <1><2><3><4><5>

    Figure 9-8   Initialization, adding and removing edges, adding and removing vertices in adjacency list

    The following code shows the adjacency list implementation. Compared with Figure 9-8, the actual code differs in the following ways.

    • For convenience in adding and removing vertices, and to simplify the code, we use lists (dynamic arrays) instead of linked lists.
    • A hash table is used to store the adjacency list, where key is the vertex instance and value is the list (linked list) of adjacent vertices for that vertex.

    Additionally, we use the Vertex class to represent vertices in the adjacency list for the following reason: if we used list indices to distinguish different vertices, as with adjacency matrices, then to delete the vertex at index \\(i\\), we would need to traverse the entire adjacency list and decrement all indices greater than \\(i\\) by \\(1\\), which is very inefficient. However, if each vertex is a unique Vertex instance, deleting one vertex does not require modifying the others.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py
    class GraphAdjList:\n    \"\"\"Undirected graph class based on adjacency list\"\"\"\n\n    def __init__(self, edges: list[list[Vertex]]):\n        \"\"\"Constructor\"\"\"\n        # Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n        self.adj_list = dict[Vertex, list[Vertex]]()\n        # Add all vertices and edges\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        \"\"\"Get the number of vertices\"\"\"\n        return len(self.adj_list)\n\n    def add_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Add edge\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Add edge 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        \"\"\"Remove edge\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Remove edge 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        \"\"\"Add vertex\"\"\"\n        if vet in self.adj_list:\n            return\n        # Add a new linked list in the adjacency list\n        self.adj_list[vet] = []\n\n    def remove_vertex(self, vet: Vertex):\n        \"\"\"Remove vertex\"\"\"\n        if vet not in self.adj_list:\n            raise ValueError()\n        # Remove the linked list corresponding to vertex vet in the adjacency list\n        self.adj_list.pop(vet)\n        # Traverse the linked lists of other vertices and remove all edges containing 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        \"\"\"Print adjacency list\"\"\"\n        print(\"Adjacency list =\")\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n  public:\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n    /* Remove specified node from 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    /* Constructor */\n    GraphAdjList(const vector<vector<Vertex *>> &edges) {\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    int size() {\n        return adjList.size();\n    }\n\n    /* Add edge */\n    void addEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"Vertex does not exist\");\n        // Add edge vet1 - vet2\n        adjList[vet1].push_back(vet2);\n        adjList[vet2].push_back(vet1);\n    }\n\n    /* Remove edge */\n    void removeEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"Vertex does not exist\");\n        // Remove edge vet1 - vet2\n        remove(adjList[vet1], vet2);\n        remove(adjList[vet2], vet1);\n    }\n\n    /* Add vertex */\n    void addVertex(Vertex *vet) {\n        if (adjList.count(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList[vet] = vector<Vertex *>();\n    }\n\n    /* Remove vertex */\n    void removeVertex(Vertex *vet) {\n        if (!adjList.count(vet))\n            throw invalid_argument(\"Vertex does not exist\");\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.erase(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (auto &adj : adjList) {\n            remove(adj.second, vet);\n        }\n    }\n\n    /* Print adjacency list */\n    void print() {\n        cout << \"Adjacency list =\" << 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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    Map<Vertex, List<Vertex>> adjList;\n\n    /* Constructor */\n    public GraphAdjList(Vertex[][] edges) {\n        this.adjList = new HashMap<>();\n        // Add all vertices and edges\n        for (Vertex[] edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    public int size() {\n        return adjList.size();\n    }\n\n    /* Add edge */\n    public void addEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Add edge vet1 - vet2\n        adjList.get(vet1).add(vet2);\n        adjList.get(vet2).add(vet1);\n    }\n\n    /* Remove edge */\n    public void removeEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Remove edge vet1 - vet2\n        adjList.get(vet1).remove(vet2);\n        adjList.get(vet2).remove(vet1);\n    }\n\n    /* Add vertex */\n    public void addVertex(Vertex vet) {\n        if (adjList.containsKey(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList.put(vet, new ArrayList<>());\n    }\n\n    /* Remove vertex */\n    public void removeVertex(Vertex vet) {\n        if (!adjList.containsKey(vet))\n            throw new IllegalArgumentException();\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.remove(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (List<Vertex> list : adjList.values()) {\n            list.remove(vet);\n        }\n    }\n\n    /* Print adjacency list */\n    public void print() {\n        System.out.println(\"Adjacency list =\");\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    public Dictionary<Vertex, List<Vertex>> adjList;\n\n    /* Constructor */\n    public GraphAdjList(Vertex[][] edges) {\n        adjList = [];\n        // Add all vertices and edges\n        foreach (Vertex[] edge in edges) {\n            AddVertex(edge[0]);\n            AddVertex(edge[1]);\n            AddEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int Size() {\n        return adjList.Count;\n    }\n\n    /* Add edge */\n    public void AddEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Add edge vet1 - vet2\n        adjList[vet1].Add(vet2);\n        adjList[vet2].Add(vet1);\n    }\n\n    /* Remove edge */\n    public void RemoveEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Remove edge vet1 - vet2\n        adjList[vet1].Remove(vet2);\n        adjList[vet2].Remove(vet1);\n    }\n\n    /* Add vertex */\n    public void AddVertex(Vertex vet) {\n        if (adjList.ContainsKey(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList.Add(vet, []);\n    }\n\n    /* Remove vertex */\n    public void RemoveVertex(Vertex vet) {\n        if (!adjList.ContainsKey(vet))\n            throw new InvalidOperationException();\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.Remove(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        foreach (List<Vertex> list in adjList.Values) {\n            list.Remove(vet);\n        }\n    }\n\n    /* Print adjacency list */\n    public void Print() {\n        Console.WriteLine(\"Adjacency list =\");\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
    /* Undirected graph class based on adjacency list */\ntype graphAdjList struct {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList map[Vertex][]Vertex\n}\n\n/* Constructor */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n    g := &graphAdjList{\n        adjList: make(map[Vertex][]Vertex),\n    }\n    // Add all vertices and edges\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/* Get the number of vertices */\nfunc (g *graphAdjList) size() int {\n    return len(g.adjList)\n}\n\n/* Add edge */\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    // Add edge vet1 - vet2, add anonymous struct{},\n    g.adjList[vet1] = append(g.adjList[vet1], vet2)\n    g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* Remove edge */\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    // Remove edge vet1 - vet2\n    g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n    g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* Add vertex */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if ok {\n        return\n    }\n    // Add a new linked list in the adjacency list\n    g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* Remove vertex */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if !ok {\n        panic(\"error\")\n    }\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    delete(g.adjList, vet)\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    for v, list := range g.adjList {\n        g.adjList[v] = DeleteSliceElms(list, vet)\n    }\n}\n\n/* Print adjacency list */\nfunc (g *graphAdjList) print() {\n    var builder strings.Builder\n    fmt.Printf(\"Adjacency list = \\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    public private(set) var adjList: [Vertex: [Vertex]]\n\n    /* Constructor */\n    public init(edges: [[Vertex]]) {\n        adjList = [:]\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    public func size() -> Int {\n        adjList.count\n    }\n\n    /* Add edge */\n    public func addEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Invalid parameter\")\n        }\n        // Add edge vet1 - vet2\n        adjList[vet1]?.append(vet2)\n        adjList[vet2]?.append(vet1)\n    }\n\n    /* Remove edge */\n    public func removeEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Invalid parameter\")\n        }\n        // Remove edge vet1 - vet2\n        adjList[vet1]?.removeAll { $0 == vet2 }\n        adjList[vet2]?.removeAll { $0 == vet1 }\n    }\n\n    /* Add vertex */\n    public func addVertex(vet: Vertex) {\n        if adjList[vet] != nil {\n            return\n        }\n        // Add a new linked list in the adjacency list\n        adjList[vet] = []\n    }\n\n    /* Remove vertex */\n    public func removeVertex(vet: Vertex) {\n        if adjList[vet] == nil {\n            fatalError(\"Invalid parameter\")\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.removeValue(forKey: vet)\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for key in adjList.keys {\n            adjList[key]?.removeAll { $0 == vet }\n        }\n    }\n\n    /* Print adjacency list */\n    public func print() {\n        Swift.print(\"Adjacency list =\")\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList;\n\n    /* Constructor */\n    constructor(edges) {\n        this.adjList = new Map();\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    size() {\n        return this.adjList.size;\n    }\n\n    /* Add edge */\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        // Add edge vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Remove edge */\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        // Remove edge 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    /* Add vertex */\n    addVertex(vet) {\n        if (this.adjList.has(vet)) return;\n        // Add a new linked list in the adjacency list\n        this.adjList.set(vet, []);\n    }\n\n    /* Remove vertex */\n    removeVertex(vet) {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        this.adjList.delete(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing 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    /* Print adjacency list */\n    print() {\n        console.log('Adjacency list =');\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList: Map<Vertex, Vertex[]>;\n\n    /* Constructor */\n    constructor(edges: Vertex[][]) {\n        this.adjList = new Map();\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    size(): number {\n        return this.adjList.size;\n    }\n\n    /* Add edge */\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        // Add edge vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Remove edge */\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        // Remove edge 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    /* Add vertex */\n    addVertex(vet: Vertex): void {\n        if (this.adjList.has(vet)) return;\n        // Add a new linked list in the adjacency list\n        this.adjList.set(vet, []);\n    }\n\n    /* Remove vertex */\n    removeVertex(vet: Vertex): void {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        this.adjList.delete(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing 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    /* Print adjacency list */\n    print(): void {\n        console.log('Adjacency list =');\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n  // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n  Map<Vertex, List<Vertex>> adjList = {};\n\n  /* Constructor */\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  /* Get the number of vertices */\n  int size() {\n    return adjList.length;\n  }\n\n  /* Add edge */\n  void addEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Add edge vet1 - vet2\n    adjList[vet1]!.add(vet2);\n    adjList[vet2]!.add(vet1);\n  }\n\n  /* Remove edge */\n  void removeEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Remove edge vet1 - vet2\n    adjList[vet1]!.remove(vet2);\n    adjList[vet2]!.remove(vet1);\n  }\n\n  /* Add vertex */\n  void addVertex(Vertex vet) {\n    if (adjList.containsKey(vet)) return;\n    // Add a new linked list in the adjacency list\n    adjList[vet] = [];\n  }\n\n  /* Remove vertex */\n  void removeVertex(Vertex vet) {\n    if (!adjList.containsKey(vet)) {\n      throw ArgumentError;\n    }\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    adjList.remove(vet);\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    adjList.forEach((key, value) {\n      value.remove(vet);\n    });\n  }\n\n  /* Print adjacency list */\n  void printAdjList() {\n    print(\"Adjacency list =\");\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
    /* Undirected graph type based on adjacency list */\npub struct GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    pub adj_list: HashMap<Vertex, Vec<Vertex>>, // maybe HashSet<Vertex> for value part is better?\n}\n\nimpl GraphAdjList {\n    /* Constructor */\n    pub fn new(edges: Vec<[Vertex; 2]>) -> Self {\n        let mut graph = GraphAdjList {\n            adj_list: HashMap::new(),\n        };\n        // Add all vertices and edges\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    /* Get the number of vertices */\n    #[allow(unused)]\n    pub fn size(&self) -> usize {\n        self.adj_list.len()\n    }\n\n    /* Add edge */\n    pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Add edge 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    /* Remove edge */\n    #[allow(unused)]\n    pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Remove edge 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    /* Add vertex */\n    pub fn add_vertex(&mut self, vet: Vertex) {\n        if self.adj_list.contains_key(&vet) {\n            return;\n        }\n        // Add a new linked list in the adjacency list\n        self.adj_list.insert(vet, vec![]);\n    }\n\n    /* Remove vertex */\n    #[allow(unused)]\n    pub fn remove_vertex(&mut self, vet: Vertex) {\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        self.adj_list.remove(&vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for list in self.adj_list.values_mut() {\n            list.retain(|&v| v != vet);\n        }\n    }\n\n    /* Print adjacency list */\n    pub fn print(&self) {\n        println!(\"Adjacency list =\");\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
    /* Node structure */\ntypedef struct AdjListNode {\n    Vertex *vertex;           // Vertex\n    struct AdjListNode *next; // Successor node\n} AdjListNode;\n\n/* Find node corresponding to vertex */\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/* Add edge helper function */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n    node->vertex = vet;\n    // Head insertion\n    node->next = head->next;\n    head->next = node;\n}\n\n/* Remove edge helper function */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *pre = head;\n    AdjListNode *cur = head->next;\n    // Search for node corresponding to vet in list\n    while (cur != NULL && cur->vertex != vet) {\n        pre = cur;\n        cur = cur->next;\n    }\n    if (cur == NULL)\n        return;\n    // Remove node corresponding to vet from list\n    pre->next = cur->next;\n    // Free memory\n    free(cur);\n}\n\n/* Undirected graph class based on adjacency list */\ntypedef struct {\n    AdjListNode *heads[MAX_SIZE]; // Node array\n    int size;                     // Node count\n} GraphAdjList;\n\n/* Constructor */\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/* Destructor */\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/* Find node corresponding to vertex */\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/* Add edge */\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    // Add edge vet1 - vet2\n    addEdgeHelper(head1, vet2);\n    addEdgeHelper(head2, vet1);\n}\n\n/* Remove edge */\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    // Remove edge vet1 - vet2\n    removeEdgeHelper(head1, head2->vertex);\n    removeEdgeHelper(head2, head1->vertex);\n}\n\n/* Add vertex */\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    // Add a new linked list in the adjacency list\n    graph->heads[graph->size++] = head;\n}\n\n/* Remove vertex */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n    AdjListNode *node = findNode(graph, vet);\n    assert(node != NULL);\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    AdjListNode *cur = node, *pre = NULL;\n    while (cur) {\n        pre = cur;\n        cur = cur->next;\n        free(pre);\n    }\n    // Traverse the linked lists of other vertices and remove all edges containing 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    // Move vertices after this vertex forward to fill gap\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
    /* Undirected graph class based on adjacency list */\nclass GraphAdjList(edges: Array<Array<Vertex?>>) {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    val adjList = HashMap<Vertex, MutableList<Vertex>>()\n\n    /* Constructor */\n    init {\n        // Add all vertices and edges\n        for (edge in edges) {\n            addVertex(edge[0]!!)\n            addVertex(edge[1]!!)\n            addEdge(edge[0]!!, edge[1]!!)\n        }\n    }\n\n    /* Get the number of vertices */\n    fun size(): Int {\n        return adjList.size\n    }\n\n    /* Add edge */\n    fun addEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Add edge vet1 - vet2\n        adjList[vet1]?.add(vet2)\n        adjList[vet2]?.add(vet1)\n    }\n\n    /* Remove edge */\n    fun removeEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Remove edge vet1 - vet2\n        adjList[vet1]?.remove(vet2)\n        adjList[vet2]?.remove(vet1)\n    }\n\n    /* Add vertex */\n    fun addVertex(vet: Vertex) {\n        if (adjList.containsKey(vet))\n            return\n        // Add a new linked list in the adjacency list\n        adjList[vet] = mutableListOf()\n    }\n\n    /* Remove vertex */\n    fun removeVertex(vet: Vertex) {\n        if (!adjList.containsKey(vet))\n            throw IllegalArgumentException()\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.remove(vet)\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (list in adjList.values) {\n            list.remove(vet)\n        }\n    }\n\n    /* Print adjacency list */\n    fun print() {\n        println(\"Adjacency list =\")\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
    ### Undirected graph class based on adjacency list ###\nclass GraphAdjList\n  attr_reader :adj_list\n\n  ### Constructor ###\n  def initialize(edges)\n    # Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    @adj_list = {}\n    # Add all vertices and edges\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  ### Get number of vertices ###\n  def size\n    @adj_list.length\n  end\n\n  ### Add edge ###\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  ### Delete edge ###\n  def remove_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    # Remove edge vet1 - vet2\n    @adj_list[vet1].delete(vet2)\n    @adj_list[vet2].delete(vet1)\n  end\n\n  ### Add vertex ###\n  def add_vertex(vet)\n    return if @adj_list.include?(vet)\n\n    # Add a new linked list in the adjacency list\n    @adj_list[vet] = []\n  end\n\n  ### Delete vertex ###\n  def remove_vertex(vet)\n    raise ArgumentError unless @adj_list.include?(vet)\n\n    # Remove the linked list corresponding to vertex vet in the adjacency list\n    @adj_list.delete(vet)\n    # Traverse the linked lists of other vertices and remove all edges containing 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  ### Print adjacency list ###\n  def __print__\n    puts 'Adjacency list ='\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":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#923-efficiency-comparison","level":2,"title":"9.2.3   Efficiency Comparison","text":"

    Assuming the graph has \\(n\\) vertices and \\(m\\) edges, Table 9-2 compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation used in this section, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables.

    Table 9-2   Comparison of adjacency matrix and adjacency list

    Adjacency matrix Adjacency list (linked list) Adjacency list (hash table) Determine adjacency \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Add an edge \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Remove an edge \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Add a vertex \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) Remove a vertex \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) Memory space usage \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\)

    Observing Table 9-2, it appears that the adjacency list (hash table) has the best time efficiency and space efficiency. However, in practice, operating on edges in the adjacency matrix is more efficient, requiring only a single array access or assignment operation. Overall, adjacency matrices embody the principle of \"trading space for time\", while adjacency lists embody \"trading time for space\".

    ","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   Graph Traversal","text":"

    Trees represent \"one-to-many\" relationships, while graphs have a higher degree of freedom and can represent any \"many-to-many\" relationships. Therefore, we can view trees as a special case of graphs. Clearly, tree traversal operations are also a special case of graph traversal operations.

    Both graphs and trees require the application of search algorithms to implement traversal operations. Graph traversal methods can also be divided into two types: breadth-first traversal and depth-first traversal.

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931-breadth-first-search","level":2,"title":"9.3.1   Breadth-First Search","text":"

    Breadth-first search proceeds from near to far: starting from a given node, it always visits the nearest vertices first and expands outward layer by layer. As shown in Figure 9-9, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited.

    Figure 9-9   Breadth-first search of a graph

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1-algorithm-implementation","level":3,"title":"1.   Algorithm Implementation","text":"

    BFS is typically implemented with the help of a queue, as shown in the code below. The queue has a \"first in, first out\" property, which aligns with the BFS idea of \"near to far\".

    1. Add the starting vertex startVet to the queue and begin the loop.
    2. In each iteration of the loop, pop the vertex at the front of the queue and record it as visited, then add all adjacent vertices of that vertex to the back of the queue.
    3. Repeat step 2. until all vertices have been visited.

    To prevent revisiting vertices, we use a hash set visited to record which nodes have been visited.

    Tip

    A hash set can be viewed as a hash table that stores only key without storing value. It supports insertion, deletion, lookup, and update operations on key in \\(O(1)\\) time. Based on the uniqueness of key, hash sets are typically used for data deduplication and similar scenarios.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py
    def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Breadth-first traversal\"\"\"\n    # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n    # Vertex traversal sequence\n    res = []\n    # Hash set for recording vertices that have been visited\n    visited = set[Vertex]([start_vet])\n    # Queue used to implement BFS\n    que = deque[Vertex]([start_vet])\n    # Starting from vertex vet, loop until all vertices are visited\n    while len(que) > 0:\n        vet = que.popleft()  # Dequeue the front vertex\n        res.append(vet)  # Record visited vertex\n        # Traverse all adjacent vertices of this vertex\n        for adj_vet in graph.adj_list[vet]:\n            if adj_vet in visited:\n                continue  # Skip vertices that have been visited\n            que.append(adj_vet)  # Only enqueue unvisited vertices\n            visited.add(adj_vet)  # Mark this vertex as visited\n    # Return vertex traversal sequence\n    return res\n
    graph_bfs.cpp
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n    // Vertex traversal sequence\n    vector<Vertex *> res;\n    // Hash set for recording vertices that have been visited\n    unordered_set<Vertex *> visited = {startVet};\n    // Queue used to implement BFS\n    queue<Vertex *> que;\n    que.push(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.empty()) {\n        Vertex *vet = que.front();\n        que.pop();          // Dequeue the front vertex\n        res.push_back(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (auto adjVet : graph.adjList[vet]) {\n            if (visited.count(adjVet))\n                continue;            // Skip vertices that have been visited\n            que.push(adjVet);        // Only enqueue unvisited vertices\n            visited.emplace(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.java
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = new ArrayList<>();\n    // Hash set for recording vertices that have been visited\n    Set<Vertex> visited = new HashSet<>();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    Queue<Vertex> que = new LinkedList<>();\n    que.offer(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.isEmpty()) {\n        Vertex vet = que.poll(); // Dequeue the front vertex\n        res.add(vet);            // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (Vertex adjVet : graph.adjList.get(vet)) {\n            if (visited.contains(adjVet))\n                continue;        // Skip vertices that have been visited\n            que.offer(adjVet);   // Only enqueue unvisited vertices\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.cs
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = [];\n    // Hash set for recording vertices that have been visited\n    HashSet<Vertex> visited = [startVet];\n    // Queue used to implement BFS\n    Queue<Vertex> que = new();\n    que.Enqueue(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.Count > 0) {\n        Vertex vet = que.Dequeue(); // Dequeue the front vertex\n        res.Add(vet);               // Record visited vertex\n        foreach (Vertex adjVet in graph.adjList[vet]) {\n            if (visited.Contains(adjVet)) {\n                continue;          // Skip vertices that have been visited\n            }\n            que.Enqueue(adjVet);   // Only enqueue unvisited vertices\n            visited.Add(adjVet);   // Mark this vertex as visited\n        }\n    }\n\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.go
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Vertex traversal sequence\n    res := make([]Vertex, 0)\n    // Hash set for recording vertices that have been visited\n    visited := make(map[Vertex]struct{})\n    visited[startVet] = struct{}{}\n    // Queue used to implement BFS, using slice to simulate queue\n    queue := make([]Vertex, 0)\n    queue = append(queue, startVet)\n    // Starting from vertex vet, loop until all vertices are visited\n    for len(queue) > 0 {\n        // Dequeue the front vertex\n        vet := queue[0]\n        queue = queue[1:]\n        // Record visited vertex\n        res = append(res, vet)\n        // Traverse all adjacent vertices of this vertex\n        for _, adjVet := range g.adjList[vet] {\n            _, isExist := visited[adjVet]\n            // Only enqueue unvisited vertices\n            if !isExist {\n                queue = append(queue, adjVet)\n                visited[adjVet] = struct{}{}\n            }\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
    graph_bfs.swift
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Vertex traversal sequence\n    var res: [Vertex] = []\n    // Hash set for recording vertices that have been visited\n    var visited: Set<Vertex> = [startVet]\n    // Queue used to implement BFS\n    var que: [Vertex] = [startVet]\n    // Starting from vertex vet, loop until all vertices are visited\n    while !que.isEmpty {\n        let vet = que.removeFirst() // Dequeue the front vertex\n        res.append(vet) // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for adjVet in graph.adjList[vet] ?? [] {\n            if visited.contains(adjVet) {\n                continue // Skip vertices that have been visited\n            }\n            que.append(adjVet) // Only enqueue unvisited vertices\n            visited.insert(adjVet) // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
    graph_bfs.js
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphBFS(graph, startVet) {\n    // Vertex traversal sequence\n    const res = [];\n    // Hash set for recording vertices that have been visited\n    const visited = new Set();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    const que = [startVet];\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.length) {\n        const vet = que.shift(); // Dequeue the front vertex\n        res.push(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Skip vertices that have been visited\n            }\n            que.push(adjVet); // Only enqueue unvisited vertices\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.ts
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Vertex traversal sequence\n    const res: Vertex[] = [];\n    // Hash set for recording vertices that have been visited\n    const visited: Set<Vertex> = new Set();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    const que = [startVet];\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.length) {\n        const vet = que.shift(); // Dequeue the front vertex\n        res.push(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Skip vertices that have been visited\n            }\n            que.push(adjVet); // Only enqueue unvisited\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
    graph_bfs.dart
    /* Breadth-first traversal */\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n  // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  // Vertex traversal sequence\n  List<Vertex> res = [];\n  // Hash set for recording vertices that have been visited\n  Set<Vertex> visited = {};\n  visited.add(startVet);\n  // Queue used to implement BFS\n  Queue<Vertex> que = Queue();\n  que.add(startVet);\n  // Starting from vertex vet, loop until all vertices are visited\n  while (que.isNotEmpty) {\n    Vertex vet = que.removeFirst(); // Dequeue the front vertex\n    res.add(vet); // Record visited vertex\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex adjVet in graph.adjList[vet]!) {\n      if (visited.contains(adjVet)) {\n        continue; // Skip vertices that have been visited\n      }\n      que.add(adjVet); // Only enqueue unvisited vertices\n      visited.add(adjVet); // Mark this vertex as visited\n    }\n  }\n  // Return vertex traversal sequence\n  return res;\n}\n
    graph_bfs.rs
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Vertex traversal sequence\n    let mut res = vec![];\n    // Hash set for recording vertices that have been visited\n    let mut visited = HashSet::new();\n    visited.insert(start_vet);\n    // Queue used to implement BFS\n    let mut que = VecDeque::new();\n    que.push_back(start_vet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while let Some(vet) = que.pop_front() {\n        res.push(vet); // Record visited vertex\n\n        // Traverse all adjacent vertices of this vertex\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; // Skip vertices that have been visited\n                }\n                que.push_back(adj_vet); // Only enqueue unvisited vertices\n                visited.insert(adj_vet); // Mark this vertex as visited\n            }\n        }\n    }\n    // Return vertex traversal sequence\n    res\n}\n
    graph_bfs.c
    /* Node queue structure */\ntypedef struct {\n    Vertex *vertices[MAX_SIZE];\n    int front, rear, size;\n} Queue;\n\n/* Constructor */\nQueue *newQueue() {\n    Queue *q = (Queue *)malloc(sizeof(Queue));\n    q->front = q->rear = q->size = 0;\n    return q;\n}\n\n/* Check if the queue is empty */\nint isEmpty(Queue *q) {\n    return q->size == 0;\n}\n\n/* Enqueue operation */\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/* Dequeue operation */\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/* Check if vertex has been visited */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n    // Traverse to find node using O(n) time\n    for (int i = 0; i < size; i++) {\n        if (visited[i] == vet)\n            return 1;\n    }\n    return 0;\n}\n\n/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n    // Queue used to implement BFS\n    Queue *queue = newQueue();\n    enqueue(queue, startVet);\n    visited[(*visitedSize)++] = startVet;\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!isEmpty(queue)) {\n        Vertex *vet = dequeue(queue); // Dequeue the front vertex\n        res[(*resSize)++] = vet;      // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        AdjListNode *node = findNode(graph, vet);\n        while (node != NULL) {\n            // Skip vertices that have been visited\n            if (!isVisited(visited, *visitedSize, node->vertex)) {\n                enqueue(queue, node->vertex);             // Only enqueue unvisited vertices\n                visited[(*visitedSize)++] = node->vertex; // Mark this vertex as visited\n            }\n            node = node->next;\n        }\n    }\n    // Free memory\n    free(queue);\n}\n
    graph_bfs.kt
    /* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {\n    // Vertex traversal sequence\n    val res = mutableListOf<Vertex?>()\n    // Hash set for recording vertices that have been visited\n    val visited = HashSet<Vertex>()\n    visited.add(startVet)\n    // Queue used to implement BFS\n    val que = LinkedList<Vertex>()\n    que.offer(startVet)\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.isEmpty()) {\n        val vet = que.poll() // Dequeue the front vertex\n        res.add(vet)         // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (adjVet in graph.adjList[vet]!!) {\n            if (visited.contains(adjVet))\n                continue        // Skip vertices that have been visited\n            que.offer(adjVet)   // Only enqueue unvisited vertices\n            visited.add(adjVet) // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
    graph_bfs.rb
    ### Breadth-first traversal ###\ndef graph_bfs(graph, start_vet)\n  # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  # Vertex traversal sequence\n  res = []\n  # Hash set for recording vertices that have been visited\n  visited = Set.new([start_vet])\n  # Queue used to implement BFS\n  que = [start_vet]\n  # Starting from vertex vet, loop until all vertices are visited\n  while que.length > 0\n    vet = que.shift # Dequeue the front vertex\n    res << vet # Record visited vertex\n    # Traverse all adjacent vertices of this vertex\n    for adj_vet in graph.adj_list[vet]\n      next if visited.include?(adj_vet) # Skip vertices that have been visited\n      que << adj_vet # Only enqueue unvisited vertices\n      visited.add(adj_vet) # Mark this vertex as visited\n    end\n  end\n  # Return vertex traversal sequence\n  res\nend\n

    The code is relatively abstract; it is recommended to refer to Figure 9-10 to deepen understanding.

    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 9-10   Steps of breadth-first search of a graph

    Is the breadth-first traversal sequence unique?

    Not unique. Breadth-first search only requires traversing in a \"near to far\" order, and the traversal order of vertices at the same distance can be arbitrarily shuffled. Taking Figure 9-10 as an example, the visit order of vertices \\(1\\) and \\(3\\) can be swapped, as can the visit order of vertices \\(2\\), \\(4\\), and \\(6\\).

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2-complexity-analysis","level":3,"title":"2.   Complexity Analysis","text":"

    Time complexity: All vertices will be enqueued and dequeued once, using \\(O(|V|)\\) time; in the process of traversing adjacent vertices, since it is an undirected graph, all edges will be visited \\(2\\) times, using \\(O(2|E|)\\) time; overall using \\(O(|V| + |E|)\\) time.

    Space complexity: The list res, hash set visited, and queue que can contain at most \\(|V|\\) vertices, using \\(O(|V|)\\) space.

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932-depth-first-search","level":2,"title":"9.3.2   Depth-First Search","text":"

    Depth-first search is a traversal method that prioritizes going as far as possible, then backtracks when no path remains. As shown in Figure 9-11, starting from the top-left vertex, visit an adjacent vertex of the current vertex, continuing until reaching a dead end, then return and continue going as far as possible before returning again, and so on, until all vertices have been traversed.

    Figure 9-11   Depth-first search of a graph

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1-algorithm-implementation_1","level":3,"title":"1.   Algorithm Implementation","text":"

    This \"go as far as possible then return\" algorithm paradigm is typically implemented using recursion. Similar to breadth-first search, in depth-first search we also need a hash set visited to record visited vertices and avoid revisiting.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py
    def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n    \"\"\"Depth-first traversal helper function\"\"\"\n    res.append(vet)  # Record visited vertex\n    visited.add(vet)  # Mark this vertex as visited\n    # Traverse all adjacent vertices of this vertex\n    for adjVet in graph.adj_list[vet]:\n        if adjVet in visited:\n            continue  # Skip vertices that have been visited\n        # Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Depth-first traversal\"\"\"\n    # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n    # Vertex traversal sequence\n    res = []\n    # Hash set for recording vertices that have been visited\n    visited = set[Vertex]()\n    dfs(graph, visited, res, start_vet)\n    return res\n
    graph_dfs.cpp
    /* Depth-first traversal helper function */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n    res.push_back(vet);   // Record visited vertex\n    visited.emplace(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex *adjVet : graph.adjList[vet]) {\n        if (visited.count(adjVet))\n            continue; // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n    // Vertex traversal sequence\n    vector<Vertex *> res;\n    // Hash set for recording vertices that have been visited\n    unordered_set<Vertex *> visited;\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.java
    /* Depth-first traversal helper function */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.add(vet);     // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex adjVet : graph.adjList.get(vet)) {\n        if (visited.contains(adjVet))\n            continue; // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = new ArrayList<>();\n    // Hash set for recording vertices that have been visited\n    Set<Vertex> visited = new HashSet<>();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.cs
    /* Depth-first traversal helper function */\nvoid DFS(GraphAdjList graph, HashSet<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.Add(vet);     // Record visited vertex\n    visited.Add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    foreach (Vertex adjVet in graph.adjList[vet]) {\n        if (visited.Contains(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        DFS(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = [];\n    // Hash set for recording vertices that have been visited\n    HashSet<Vertex> visited = [];\n    DFS(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.go
    /* Depth-first traversal helper function */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n    // append operation returns a new reference, must reassign original reference to new slice's reference\n    *res = append(*res, vet)\n    visited[vet] = struct{}{}\n    // Traverse all adjacent vertices of this vertex\n    for _, adjVet := range g.adjList[vet] {\n        _, isExist := visited[adjVet]\n        // Recursively visit adjacent vertices\n        if !isExist {\n            dfs(g, visited, res, adjVet)\n        }\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Vertex traversal sequence\n    res := make([]Vertex, 0)\n    // Hash set for recording vertices that have been visited\n    visited := make(map[Vertex]struct{})\n    dfs(g, visited, &res, startVet)\n    // Return vertex traversal sequence\n    return res\n}\n
    graph_dfs.swift
    /* Depth-first traversal helper function */\nfunc dfs(graph: GraphAdjList, visited: inout Set<Vertex>, res: inout [Vertex], vet: Vertex) {\n    res.append(vet) // Record visited vertex\n    visited.insert(vet) // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for adjVet in graph.adjList[vet] ?? [] {\n        if visited.contains(adjVet) {\n            continue // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph: graph, visited: &visited, res: &res, vet: adjVet)\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Vertex traversal sequence\n    var res: [Vertex] = []\n    // Hash set for recording vertices that have been visited\n    var visited: Set<Vertex> = []\n    dfs(graph: graph, visited: &visited, res: &res, vet: startVet)\n    return res\n}\n
    graph_dfs.js
    /* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction dfs(graph, visited, res, vet) {\n    res.push(vet); // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphDFS(graph, startVet) {\n    // Vertex traversal sequence\n    const res = [];\n    // Hash set for recording vertices that have been visited\n    const visited = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.ts
    /* Depth-first traversal helper function */\nfunction dfs(\n    graph: GraphAdjList,\n    visited: Set<Vertex>,\n    res: Vertex[],\n    vet: Vertex\n): void {\n    res.push(vet); // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Vertex traversal sequence\n    const res: Vertex[] = [];\n    // Hash set for recording vertices that have been visited\n    const visited: Set<Vertex> = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
    graph_dfs.dart
    /* Depth-first traversal helper function */\nvoid dfs(\n  GraphAdjList graph,\n  Set<Vertex> visited,\n  List<Vertex> res,\n  Vertex vet,\n) {\n  res.add(vet); // Record visited vertex\n  visited.add(vet); // Mark this vertex as visited\n  // Traverse all adjacent vertices of this vertex\n  for (Vertex adjVet in graph.adjList[vet]!) {\n    if (visited.contains(adjVet)) {\n      continue; // Skip vertices that have been visited\n    }\n    // Recursively visit adjacent vertices\n    dfs(graph, visited, res, adjVet);\n  }\n}\n\n/* Depth-first traversal */\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n  // Vertex traversal sequence\n  List<Vertex> res = [];\n  // Hash set for recording vertices that have been visited\n  Set<Vertex> visited = {};\n  dfs(graph, visited, res, startVet);\n  return res;\n}\n
    graph_dfs.rs
    /* Depth-first traversal helper function */\nfn dfs(graph: &GraphAdjList, visited: &mut HashSet<Vertex>, res: &mut Vec<Vertex>, vet: Vertex) {\n    res.push(vet); // Record visited vertex\n    visited.insert(vet); // Mark this vertex as visited\n                         // Traverse all adjacent vertices of this vertex\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; // Skip vertices that have been visited\n            }\n            // Recursively visit adjacent vertices\n            dfs(graph, visited, res, adj_vet);\n        }\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Vertex traversal sequence\n    let mut res = vec![];\n    // Hash set for recording vertices that have been visited\n    let mut visited = HashSet::new();\n    dfs(&graph, &mut visited, &mut res, start_vet);\n\n    res\n}\n
    graph_dfs.c
    /* Check if vertex has been visited */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n    // Traverse to find node using O(n) time\n    for (int i = 0; i < size; i++) {\n        if (res[i] == vet) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/* Depth-first traversal helper function */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n    // Record visited vertex\n    res[(*resSize)++] = vet;\n    // Traverse all adjacent vertices of this vertex\n    AdjListNode *node = findNode(graph, vet);\n    while (node != NULL) {\n        // Skip vertices that have been visited\n        if (!isVisited(res, *resSize, node->vertex)) {\n            // Recursively visit adjacent vertices\n            dfs(graph, res, resSize, node->vertex);\n        }\n        node = node->next;\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n    dfs(graph, res, resSize, startVet);\n}\n
    graph_dfs.kt
    /* Depth-first traversal helper function */\nfun dfs(\n    graph: GraphAdjList,\n    visited: MutableSet<Vertex?>,\n    res: MutableList<Vertex?>,\n    vet: Vertex?\n) {\n    res.add(vet)     // Record visited vertex\n    visited.add(vet) // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (adjVet in graph.adjList[vet]!!) {\n        if (visited.contains(adjVet))\n            continue  // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet)\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {\n    // Vertex traversal sequence\n    val res = mutableListOf<Vertex?>()\n    // Hash set for recording vertices that have been visited\n    val visited = HashSet<Vertex?>()\n    dfs(graph, visited, res, startVet)\n    return res\n}\n
    graph_dfs.rb
    ### Depth-first traversal helper function ###\ndef dfs(graph, visited, res, vet)\n  res << vet # Record visited vertex\n  visited.add(vet) # Mark this vertex as visited\n  # Traverse all adjacent vertices of this vertex\n  for adj_vet in graph.adj_list[vet]\n    next if visited.include?(adj_vet) # Skip vertices that have been visited\n    # Recursively visit adjacent vertices\n    dfs(graph, visited, res, adj_vet)\n  end\nend\n\n### Depth-first traversal ###\ndef graph_dfs(graph, start_vet)\n  # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  # Vertex traversal sequence\n  res = []\n  # Hash set for recording vertices that have been visited\n  visited = Set.new\n  dfs(graph, visited, res, start_vet)\n  res\nend\n

    The algorithm flow of depth-first search is shown in Figure 9-12.

    • Straight dashed lines represent downward recursion, indicating that a new recursive method has been initiated to visit a new vertex.
    • Curved dashed lines represent upward backtracking, indicating that this recursive call has returned to the point where it was made.

    To deepen understanding, it is recommended to combine Figure 9-12 with the code to mentally simulate (or draw out) the entire DFS process, including when each recursive call begins and when it returns.

    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 9-12   Steps of depth-first search of a graph

    Is the depth-first traversal sequence unique?

    Similar to breadth-first search, depth-first traversal sequences are also not unique. Given a vertex, any exploration direction may be chosen first; that is, the order of adjacent vertices can be arbitrarily rearranged and still constitute depth-first search.

    Taking tree traversal as an example, \"root \\(\\rightarrow\\) left \\(\\rightarrow\\) right\", \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\", and \"left \\(\\rightarrow\\) right \\(\\rightarrow\\) root\" correspond to pre-order, in-order, and post-order traversals, respectively. They represent three different traversal priorities, yet all three belong to depth-first search.

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2-complexity-analysis_1","level":3,"title":"2.   Complexity Analysis","text":"

    Time complexity: All vertices will be visited \\(1\\) time, using \\(O(|V|)\\) time; all edges will be visited \\(2\\) times, using \\(O(2|E|)\\) time; overall using \\(O(|V| + |E|)\\) time.

    Space complexity: The list res and hash set visited can contain at most \\(|V|\\) vertices, and the maximum recursion depth is \\(|V|\\), therefore using \\(O(|V|)\\) space.

    ","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   Summary","text":"","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_graph/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Graphs consist of vertices and edges and can be represented as a set of vertices and a set of edges.
    • Compared with the linear relationships modeled by linked lists and the divide-and-conquer relationships modeled by trees, the network relationships modeled by graphs offer much greater flexibility and are therefore more complex.
    • In directed graphs, edges have direction; in connected graphs, every vertex is reachable from any other vertex; and in weighted graphs, each edge carries a weight.
    • Adjacency matrices use matrices to represent graphs, where each row (column) represents a vertex, and matrix elements represent edges, using \\(1\\) or \\(0\\) to indicate whether two vertices have an edge or not. Adjacency matrices are highly efficient for addition, deletion, lookup, and modification operations, but consume significant space.
    • Adjacency lists use multiple linked lists to represent a graph: the \\(i\\)-th linked list corresponds to vertex \\(i\\) and stores all vertices adjacent to it. Compared with adjacency matrices, adjacency lists use less space, but edge lookups are less efficient because the linked list must be traversed.
    • When linked lists in adjacency lists become too long, they can be converted to red-black trees or hash tables, thereby improving lookup efficiency.
    • From an algorithmic perspective, adjacency matrices embody \"trading space for time\", while adjacency lists embody \"trading time for space\".
    • Graphs can be used to model various real-world systems, such as social networks and subway lines.
    • Trees are a special case of graphs, and tree traversal is a special case of graph traversal.
    • Breadth-first search in graphs explores from near to far, expanding layer by layer, and is typically implemented with a queue.
    • Depth-first search in graphs follows a path as deep as possible and backtracks when it can go no farther, and is commonly implemented with recursion.
    ","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Is a path defined as a sequence of vertices or a sequence of edges?

    The definitions in different language versions of Wikipedia are inconsistent: the English version states \"a path is a sequence of edges\", while the Chinese version states \"a path is a sequence of vertices\". The following is the original English text: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

    In this text, a path is viewed as a sequence of edges, not a sequence of vertices. This is because there may be multiple edges connecting two vertices, in which case each edge corresponds to a path.

    Q: In a disconnected graph, will there be unreachable vertices?

    In a disconnected graph, if you start from one vertex, at least one other vertex will be unreachable. To traverse a disconnected graph, you need multiple starting points so that all connected components are covered.

    Q: In an adjacency list, is there any required ordering for the vertices adjacent to a given vertex?

    They can appear in any order. In practice, however, they may need to be sorted according to specific rules, such as the order in which vertices were added or the order of vertex values, which helps when quickly finding a vertex with some extreme value.

    ","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"Chapter 15.   Greedy","text":"

    Abstract

    Sunflowers turn toward the sun, always seeking the fullest growth possible.

    Through successive simple choices, greedy strategies gradually lead to the optimal solution.

    ","path":["Chapter 15. Greedy","Chapter 15.   Greedy"],"tags":[]},{"location":"chapter_greedy/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 15.1   Greedy Algorithm
    • 15.2   Fractional Knapsack Problem
    • 15.3   Maximum Capacity Problem
    • 15.4   Maximum Product Cutting Problem
    • 15.5   Summary
    ","path":["Chapter 15. Greedy","Chapter 15.   Greedy"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   Fractional Knapsack Problem","text":"

    Question

    Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can be selected only once, but a fraction of an item may be selected, with its value proportional to the selected weight. What is the maximum total value that can be placed in the knapsack under the capacity constraint? An example is shown in Figure 15-3.

    Figure 15-3   Example data for the fractional knapsack problem

    The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, with states including the current item \\(i\\) and capacity \\(c\\), and the goal being to maximize value under the limited knapsack capacity.

    The difference is that this problem allows selecting only a fraction of an item. As shown in Figure 15-4, we can split an item arbitrarily and compute its value in proportion to the selected weight.

    1. For item \\(i\\), its value per unit weight is \\(val[i-1] / wgt[i-1]\\), referred to as unit value.
    2. Suppose we put a portion of item \\(i\\) with weight \\(w\\) into the knapsack, then the value added to the knapsack is \\(w \\times val[i-1] / wgt[i-1]\\).

    Figure 15-4   Value of items per unit weight

    ","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

    Maximizing the total value in the knapsack essentially means prioritizing items with higher value per unit weight. From this observation, we can derive the greedy strategy shown in Figure 15-5.

    1. Sort items by unit value from high to low.
    2. Iterate through all items, greedily selecting the item with the highest unit value in each round.
    3. If the remaining knapsack capacity is insufficient, use a portion of the current item to fill the knapsack.

    Figure 15-5   Greedy strategy for the fractional knapsack problem

    ","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    We define an Item class so that items can be sorted by unit value. We then iterate through the sorted items greedily, stopping once the knapsack is full and returning the result:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py
    class Item:\n    \"\"\"Item\"\"\"\n\n    def __init__(self, w: int, v: int):\n        self.w = w  # Item weight\n        self.v = v  # Item value\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Fractional knapsack: Greedy algorithm\"\"\"\n    # Create item list with two attributes: weight, value\n    items = [Item(w, v) for w, v in zip(wgt, val)]\n    # Sort by unit value item.v / item.w from high to low\n    items.sort(key=lambda item: item.v / item.w, reverse=True)\n    # Loop for greedy selection\n    res = 0\n    for item in items:\n        if item.w <= cap:\n            # If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v\n            cap -= item.w\n        else:\n            # If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap\n            # No remaining capacity, so break out of the loop\n            break\n    return res\n
    fractional_knapsack.cpp
    /* Item */\nclass Item {\n  public:\n    int w; // Item weight\n    int v; // Item value\n\n    Item(int w, int v) : w(w), v(v) {\n    }\n};\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n    // Create item list with two attributes: weight, value\n    vector<Item> items;\n    for (int i = 0; i < wgt.size(); i++) {\n        items.push_back(Item(wgt[i], val[i]));\n    }\n    // Sort by unit value item.v / item.w from high to low\n    sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n    // Loop for greedy selection\n    double res = 0;\n    for (auto &item : items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double)item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.java
    /* Item */\nclass Item {\n    int w; // Item weight\n    int v; // Item value\n\n    public Item(int w, int v) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Create item list with two attributes: weight, value\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    // Sort by unit value item.v / item.w from high to low\n    Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n    // Loop for greedy selection\n    double res = 0;\n    for (Item item : items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double) item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.cs
    /* Item */\nclass Item(int w, int v) {\n    public int w = w; // Item weight\n    public int v = v; // Item value\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Create item list with two attributes: weight, value\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    // Sort by unit value item.v / item.w from high to low\n    Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));\n    // Loop for greedy selection\n    double res = 0;\n    foreach (Item item in items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double)item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.go
    /* Item */\ntype Item struct {\n    w int // Item weight\n    v int // Item value\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n    // Create item list with two attributes: weight, value\n    items := make([]Item, len(wgt))\n    for i := 0; i < len(wgt); i++ {\n        items[i] = Item{wgt[i], val[i]}\n    }\n    // Sort by unit value item.v / item.w from high to low\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    // Loop for greedy selection\n    res := 0.0\n    for _, item := range items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += float64(item.v)\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += float64(item.v) / float64(item.w) * float64(cap)\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
    fractional_knapsack.swift
    /* Item */\nclass Item {\n    var w: Int // Item weight\n    var v: Int // Item value\n\n    init(w: Int, v: Int) {\n        self.w = w\n        self.v = v\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {\n    // Create item list with two attributes: weight, value\n    var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n    // Sort by unit value item.v / item.w from high to low\n    items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }\n    // Loop for greedy selection\n    var res = 0.0\n    var cap = cap\n    for item in items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += Double(item.v)\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += Double(item.v) / Double(item.w) * Double(cap)\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
    fractional_knapsack.js
    /* Item */\nclass Item {\n    constructor(w, v) {\n        this.w = w; // Item weight\n        this.v = v; // Item value\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunction fractionalKnapsack(wgt, val, cap) {\n    // Create item list with two attributes: weight, value\n    const items = wgt.map((w, i) => new Item(w, val[i]));\n    // Sort by unit value item.v / item.w from high to low\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Loop for greedy selection\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.ts
    /* Item */\nclass Item {\n    w: number; // Item weight\n    v: number; // Item value\n\n    constructor(w: number, v: number) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n    // Create item list with two attributes: weight, value\n    const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));\n    // Sort by unit value item.v / item.w from high to low\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Loop for greedy selection\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
    fractional_knapsack.dart
    /* Item */\nclass Item {\n  int w; // Item weight\n  int v; // Item value\n\n  Item(this.w, this.v);\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(List<int> wgt, List<int> val, int cap) {\n  // Create item list with two attributes: weight, value\n  List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));\n  // Sort by unit value item.v / item.w from high to low\n  items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));\n  // Loop for greedy selection\n  double res = 0;\n  for (Item item in items) {\n    if (item.w <= cap) {\n      // If remaining capacity is sufficient, put the entire current item into the knapsack\n      res += item.v;\n      cap -= item.w;\n    } else {\n      // If remaining capacity is insufficient, put part of the current item into the knapsack\n      res += item.v / item.w * cap;\n      // No remaining capacity, so break out of the loop\n      break;\n    }\n  }\n  return res;\n}\n
    fractional_knapsack.rs
    /* Item */\nstruct Item {\n    w: i32, // Item weight\n    v: i32, // Item value\n}\n\nimpl Item {\n    fn new(w: i32, v: i32) -> Self {\n        Self { w, v }\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {\n    // Create item list with two attributes: weight, value\n    let mut items = wgt\n        .iter()\n        .zip(val.iter())\n        .map(|(&w, &v)| Item::new(w, v))\n        .collect::<Vec<Item>>();\n    // Sort by unit value item.v / item.w from high to low\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    // Loop for greedy selection\n    let mut res = 0.0;\n    for item in &items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v as f64;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += item.v as f64 / item.w as f64 * cap as f64;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    res\n}\n
    fractional_knapsack.c
    /* Item */\ntypedef struct {\n    int w; // Item weight\n    int v; // Item value\n} Item;\n\n/* Fractional knapsack: Greedy algorithm */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n    // Create item list with two attributes: weight, value\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    // Sort by unit value item.v / item.w from high to low\n    qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n    // Loop for greedy selection\n    float res = 0.0;\n    for (int i = 0; i < itemCount; i++) {\n        if (items[i].w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += items[i].v;\n            cap -= items[i].w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\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
    /* Item */\nclass Item(\n    val w: Int, // Item\n    val v: Int  // Item value\n)\n\n/* Fractional knapsack: Greedy algorithm */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n    // Create item list with two attributes: weight, value\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    // Sort by unit value item.v / item.w from high to low\n    items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }\n    // Loop for greedy selection\n    var res = 0.0\n    for (item in items) {\n        if (item!!.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += item.v.toDouble() / item.w * cap\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
    fractional_knapsack.rb
    ### Item ###\nclass Item\n  attr_accessor :w # Item weight\n  attr_accessor :v # Item value\n\n  def initialize(w, v)\n    @w = w\n    @v = v\n  end\nend\n\n### Fractional knapsack: greedy ###\ndef fractional_knapsack(wgt, val, cap)\n  # Create item list with two attributes: weight, value\n  items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n  # Sort by unit value item.v / item.w from high to low\n  items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }\n  # Loop for greedy selection\n  res = 0\n  for item in items\n    if item.w <= cap\n      # If remaining capacity is sufficient, put the entire current item into the knapsack\n      res += item.v\n      cap -= item.w\n    else\n      # If remaining capacity is insufficient, put part of the current item into the knapsack\n      res += (item.v.to_f / item.w) * cap\n      # No remaining capacity, so break out of the loop\n      break\n    end\n  end\n  res\nend\n

    Built-in sorting algorithms usually take \\(O(n \\log n)\\) time, and their space complexity is usually \\(O(\\log n)\\) or \\(O(n)\\), depending on the specific implementation of the programming language.

    Apart from sorting, in the worst case the entire item list needs to be traversed, therefore the time complexity is \\(O(n)\\), where \\(n\\) is the number of items.

    Since an Item object list is initialized, the space complexity is \\(O(n)\\).

    ","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

    We use proof by contradiction. Suppose item \\(x\\) has the highest unit value, and some algorithm produces an optimal value res, but the resulting solution does not include item \\(x\\).

    Now remove one unit of weight from any item in the knapsack and replace it with one unit of weight from item \\(x\\). Since item \\(x\\) has the highest unit value, the total value after the replacement must be greater than res. This contradicts the assumption that res is optimal, proving that any optimal solution must include item \\(x\\).

    We can construct the same contradiction for the other items in the solution as well. In summary, items with higher unit value are always the better choice, which proves that the greedy strategy is effective.

    As shown in Figure 15-6, if we treat item weight and unit value as the horizontal and vertical axes of a two-dimensional chart, then the fractional knapsack problem can be viewed as \"finding the maximum area enclosed within a bounded interval on the horizontal axis.\" This analogy helps explain the effectiveness of the greedy strategy from a geometric perspective.

    Figure 15-6   Geometric representation of the fractional knapsack problem

    ","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   Greedy Algorithm","text":"

    Greedy algorithm is a common approach to solving optimization problems. Its basic idea is to choose the option that appears best at each decision stage, that is, to greedily make locally optimal decisions in the hope of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely used in many practical problems.

    Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as both relying on the optimal substructure property, but they work differently.

    • Dynamic programming considers all previous decisions when making the current decision, and uses solutions to past subproblems to construct the solution to the current subproblem.
    • Greedy algorithms do not consider past decisions, but instead make greedy choices moving forward, continually reducing the problem size until the problem is solved.

    We will first understand how greedy algorithms work through the example problem \"coin change.\" This problem was already introduced in the \"Complete Knapsack Problem\" chapter, so it should already be familiar to you.

    Question

    Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type is \\(coins[i - 1]\\), a target amount \\(amt\\), and an unlimited number of coins of each type, what is the minimum number of coins needed to make up the target amount? If the target amount cannot be made up, return \\(-1\\).

    The greedy strategy for this problem is shown in Figure 15-1. Given a target amount, we greedily choose the coin that does not exceed it and is closest to it, repeating this step until the target amount is made up.

    Figure 15-1   Greedy strategy for coin change

    The implementation code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py
    def coin_change_greedy(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Greedy algorithm\"\"\"\n    # Assume coins list is sorted\n    i = len(coins) - 1\n    count = 0\n    # Loop to make greedy choices until no remaining amount\n    while amt > 0:\n        # Find the coin that is less than and closest to the remaining amount\n        while i > 0 and coins[i] > amt:\n            i -= 1\n        # Choose coins[i]\n        amt -= coins[i]\n        count += 1\n    # If no feasible solution is found, return -1\n    return count if amt == 0 else -1\n
    coin_change_greedy.cpp
    /* Coin change: Greedy algorithm */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.size() - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.java
    /* Coin change: Greedy algorithm */\nint coinChangeGreedy(int[] coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.length - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.cs
    /* Coin change: Greedy algorithm */\nint CoinChangeGreedy(int[] coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.Length - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.go
    /* Coin change: Greedy algorithm */\nfunc coinChangeGreedy(coins []int, amt int) int {\n    // Assume coins list is sorted\n    i := len(coins) - 1\n    count := 0\n    // Loop to make greedy choices until no remaining amount\n    for amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        for i > 0 && coins[i] > amt {\n            i--\n        }\n        // Choose coins[i]\n        amt -= coins[i]\n        count++\n    }\n    // If no feasible solution is found, return -1\n    if amt != 0 {\n        return -1\n    }\n    return count\n}\n
    coin_change_greedy.swift
    /* Coin change: Greedy algorithm */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -> Int {\n    // Assume coins list is sorted\n    var i = coins.count - 1\n    var count = 0\n    var amt = amt\n    // Loop to make greedy choices until no remaining amount\n    while amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        while i > 0 && coins[i] > amt {\n            i -= 1\n        }\n        // Choose coins[i]\n        amt -= coins[i]\n        count += 1\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1\n}\n
    coin_change_greedy.js
    /* Coin change: Greedy algorithm */\nfunction coinChangeGreedy(coins, amt) {\n    // Assume coins array is sorted\n    let i = coins.length - 1;\n    let count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt === 0 ? count : -1;\n}\n
    coin_change_greedy.ts
    /* Coin change: Greedy algorithm */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n    // Assume coins array is sorted\n    let i = coins.length - 1;\n    let count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt === 0 ? count : -1;\n}\n
    coin_change_greedy.dart
    /* Coin change: Greedy algorithm */\nint coinChangeGreedy(List<int> coins, int amt) {\n  // Assume coins list is sorted\n  int i = coins.length - 1;\n  int count = 0;\n  // Loop to make greedy choices until no remaining amount\n  while (amt > 0) {\n    // Find the coin that is less than and closest to the remaining amount\n    while (i > 0 && coins[i] > amt) {\n      i--;\n    }\n    // Choose coins[i]\n    amt -= coins[i];\n    count++;\n  }\n  // If no feasible solution is found, return -1\n  return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.rs
    /* Coin change: Greedy algorithm */\nfn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {\n    // Assume coins list is sorted\n    let mut i = coins.len() - 1;\n    let mut count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        while i > 0 && coins[i] > amt {\n            i -= 1;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count += 1;\n    }\n    // If no feasible solution is found, return -1\n    if amt == 0 {\n        count\n    } else {\n        -1\n    }\n}\n
    coin_change_greedy.c
    /* Coin change: Greedy algorithm */\nint coinChangeGreedy(int *coins, int size, int amt) {\n    // Assume coins list is sorted\n    int i = size - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
    coin_change_greedy.kt
    /* Coin change: Greedy algorithm */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n    // Assume coins list is sorted\n    var am = amt\n    var i = coins.size - 1\n    var count = 0\n    // Loop to make greedy choices until no remaining amount\n    while (am > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > am) {\n            i--\n        }\n        // Choose coins[i]\n        am -= coins[i]\n        count++\n    }\n    // If no feasible solution is found, return -1\n    return if (am == 0) count else -1\n}\n
    coin_change_greedy.rb
    ### Coin change: greedy ###\ndef coin_change_greedy(coins, amt)\n  # Assume coins list is sorted\n  i = coins.length - 1\n  count = 0\n  # Loop to make greedy choices until no remaining amount\n  while amt > 0\n    # Find the coin that is less than and closest to the remaining amount\n    while i > 0 && coins[i] > amt\n      i -= 1\n    end\n    # Choose coins[i]\n    amt -= coins[i]\n    count += 1\n  end\n  # Return -1 if no solution found\n  amt == 0 ? count : -1\nend\n

    You may find yourself exclaiming, \"So clean!\" The greedy algorithm solves the coin change problem in only about ten lines of code.

    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511-advantages-and-limitations-of-greedy-algorithms","level":2,"title":"15.1.1   Advantages and Limitations of Greedy Algorithms","text":"

    Greedy algorithms are not only straightforward to apply and easy to implement, but are also usually very efficient. In the code above, if the smallest coin denomination is \\(\\min(coins)\\), the greedy selection loop runs at most \\(amt / \\min(coins)\\) times, giving a time complexity of \\(O(amt / \\min(coins))\\). This is an order of magnitude lower than the time complexity of the dynamic programming solution, \\(O(n \\times amt)\\).

    However, for some coin denomination sets, greedy algorithms cannot find the optimal solution. Figure 15-2 shows two examples.

    • Positive example \\(coins = [1, 5, 10, 20, 50, 100]\\): With this coin set, the greedy algorithm can find the optimal solution for any \\(amt\\).
    • Counterexample \\(coins = [1, 20, 50]\\): Suppose \\(amt = 60\\). The greedy algorithm can only find the combination \\(50 + 1 \\times 10\\), using \\(11\\) coins in total, whereas dynamic programming can find the optimal solution \\(20 + 20 + 20\\) using only \\(3\\) coins.
    • Counterexample \\(coins = [1, 49, 50]\\): Suppose \\(amt = 98\\). The greedy algorithm can only find the combination \\(50 + 1 \\times 48\\), using \\(49\\) coins in total, whereas dynamic programming can find the optimal solution \\(49 + 49\\) using only \\(2\\) coins.

    Figure 15-2   Examples where greedy algorithms cannot find the optimal solution

    In other words, for the coin change problem, greedy algorithms cannot guarantee a globally optimal solution and may even produce very poor results. This problem is better solved with dynamic programming.

    In general, greedy algorithms are applicable in the following two situations.

    1. The optimal solution can be guaranteed: In this case, greedy algorithms are often the best choice because they tend to be more efficient than backtracking and dynamic programming.
    2. An approximately optimal solution can be found: Greedy algorithms are also useful in this case. For many complex problems, finding the global optimal solution is very difficult, so efficiently finding a suboptimal solution is already a very good outcome.
    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512-characteristics-of-greedy-algorithms","level":2,"title":"15.1.2   Characteristics of Greedy Algorithms","text":"

    So the question arises: what kind of problems are suitable for solving with greedy algorithms? Or in other words, under what conditions can greedy algorithms guarantee finding the optimal solution?

    Compared to dynamic programming, the conditions for using greedy algorithms are stricter, mainly focusing on two properties of the problem.

    • Greedy choice property: Only when locally optimal choices can always lead to a globally optimal solution can greedy algorithms guarantee obtaining the optimal solution.
    • Optimal substructure: The optimal solution to the original problem contains the optimal solutions to subproblems.

    Optimal substructure has already been introduced in the \"Dynamic Programming\" chapter, so we won't elaborate on it here. It's worth noting that the optimal substructure of some problems is not obvious, but they can still be solved using greedy algorithms.

    We mainly explore methods for determining the greedy choice property. Although its description seems relatively simple, in practice, for many problems, proving the greedy choice property is not easy.

    For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving that it holds is much harder. If asked, under what conditions can a coin set be solved using a greedy algorithm? We often can only rely on intuition or examples to give a vague answer, and it is difficult to provide a rigorous mathematical proof.

    Quote

    There is a paper that presents an \\(O(n^3)\\) algorithm for determining whether a coin set can be solved optimally by a greedy algorithm for any amount.

    Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.

    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513-steps-for-solving-problems-with-greedy-algorithms","level":2,"title":"15.1.3   Steps for Solving Problems with Greedy Algorithms","text":"

    The general process for solving greedy problems can be divided into the following three steps.

    1. Problem analysis: Sort out and understand the characteristics of the problem, including state definitions, optimization objectives, and constraints. This step also appears in backtracking and dynamic programming.
    2. Determine the greedy strategy: Decide how to make a greedy choice at each step. This strategy should reduce the problem size step by step and ultimately solve the entire problem.
    3. Correctness proof: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical tools such as induction or proof by contradiction.

    Determining the greedy strategy is the core step in solving such problems, but it may not be easy in practice, mainly for the following reasons.

    • Greedy strategies vary greatly from problem to problem. For many problems, the greedy strategy is fairly intuitive and can be derived through rough reasoning and experimentation. For some complex problems, however, the greedy strategy may be deeply hidden, which strongly tests one's problem-solving experience and algorithmic ability.
    • Some greedy strategies are highly deceptive. We may confidently design a greedy strategy, write the solution code, and submit it, only to find that some test cases fail. This is because the designed greedy strategy is only \"partially correct,\" as exemplified by the coin change problem discussed above.

    To ensure correctness, we should give a rigorous mathematical proof of the greedy strategy, usually using proof by contradiction or mathematical induction.

    However, correctness proofs can also be difficult. If we have no clear direction, we usually resort to debugging against test cases, revising and validating the greedy strategy step by step.

    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514-typical-problems-solved-by-greedy-algorithms","level":2,"title":"15.1.4   Typical Problems Solved by Greedy Algorithms","text":"

    Greedy algorithms are often applied to optimization problems that satisfy greedy choice property and optimal substructure. Below are some typical greedy algorithm problems.

    • Coin change problem: With certain coin combinations, greedy algorithms can always obtain the optimal solution.
    • Interval scheduling problem: Suppose you have some tasks, each taking place during a period of time, and your goal is to complete as many tasks as possible. If you always choose the task that ends earliest, then the greedy algorithm can obtain the optimal solution.
    • Fractional knapsack problem: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), then the greedy algorithm can obtain the optimal solution in some cases.
    • Stock trading problem: Given a set of historical stock prices, you can make multiple trades, but if you already hold stocks, you cannot buy again before selling, and the goal is to obtain the maximum profit.
    • Huffman coding: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree and always merging the two nodes with the lowest frequency, the resulting Huffman tree has the minimum weighted path length (encoding length).
    • Dijkstra's algorithm: It is a greedy algorithm for solving the shortest path problem from a given source vertex to all other vertices.
    ","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   Max Capacity Problem","text":"

    Question

    Given an array \\(ht\\), where each element represents the height of a vertical partition. Any two partitions in the array, together with the space between them, can form a container.

    The capacity of the container equals the product of its height and width (that is, its area), where the height is determined by the shorter partition and the width is the difference between the array indices of the two partitions.

    Select two partitions in the array such that the capacity of the resulting container is maximized, and return that maximum capacity. An example is shown in Figure 15-7.

    Figure 15-7   Example data for the max capacity problem

    The container is formed by any two partitions, so the state of this problem is the indices of the two partitions, denoted by \\([i, j]\\).

    According to the problem statement, capacity equals height multiplied by width, where the height is determined by the shorter partition and the width is the difference between the array indices of the two partitions. Let the capacity be \\(cap[i, j]\\); then we obtain the following formula:

    \\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

    Let the array length be \\(n\\). Then the number of ways to choose two partitions (that is, the total number of states) is \\(C_n^2 = \\frac{n(n - 1)}{2}\\). The most straightforward approach is to exhaustively enumerate all states to find the maximum capacity, which has a time complexity of \\(O(n^2)\\).

    ","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

    This problem has a more efficient solution. As shown in Figure 15-8, consider a state \\([i, j]\\) where \\(i < j\\) and \\(ht[i] < ht[j]\\). In this case, \\(i\\) is the shorter partition and \\(j\\) is the taller partition.

    Figure 15-8   Initial state

    As shown in Figure 15-9, if we now move the taller partition \\(j\\) inward toward the shorter partition \\(i\\), the capacity will definitely decrease.

    This is because after moving the taller partition \\(j\\), the width \\(j-i\\) definitely decreases. Since the height is determined by the shorter partition, the height can only stay the same (\\(i\\) remains the shorter partition) or decrease (\\(j\\) becomes the shorter partition after being moved).

    Figure 15-9   State after moving the long partition inward

    Conversely, only by moving the shorter partition \\(i\\) inward can the capacity possibly increase. Although the width will definitely decrease, the height may increase (the moved partition at \\(i\\) may be taller). For example, in Figure 15-10, the area increases after moving the shorter partition.

    Figure 15-10   State after moving the short partition inward

    From this, we can derive the greedy strategy for this problem: initialize two pointers at the two ends, and in each round move the pointer corresponding to the shorter partition inward until the two pointers meet.

    Figure 15-11 shows the execution process of the greedy strategy.

    1. In the initial state, pointers \\(i\\) and \\(j\\) are at both ends of the array.
    2. Calculate the capacity of the current state \\(cap[i, j]\\), and update the maximum capacity.
    3. Compare the heights of partitions \\(i\\) and \\(j\\), and move the pointer corresponding to the shorter partition inward by one position.
    4. Repeat steps 2. and 3. until \\(i\\) and \\(j\\) meet.
    <1><2><3><4><5><6><7><8><9>

    Figure 15-11   Greedy process for the max capacity problem

    ","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    The code runs for at most \\(n\\) rounds, so the time complexity is \\(O(n)\\).

    Variables \\(i\\), \\(j\\), and \\(res\\) use only a constant amount of extra space, so the space complexity is \\(O(1)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py
    def max_capacity(ht: list[int]) -> int:\n    \"\"\"Max capacity: Greedy algorithm\"\"\"\n    # Initialize i, j to be at both ends of the array\n    i, j = 0, len(ht) - 1\n    # Initial max capacity is 0\n    res = 0\n    # Loop for greedy selection until the two boards meet\n    while i < j:\n        # Update max capacity\n        cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        # Move the shorter board inward\n        if ht[i] < ht[j]:\n            i += 1\n        else:\n            j -= 1\n    return res\n
    max_capacity.cpp
    /* Max capacity: Greedy algorithm */\nint maxCapacity(vector<int> &ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.size() - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = min(ht[i], ht[j]) * (j - i);\n        res = max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
    max_capacity.java
    /* Max capacity: Greedy algorithm */\nint maxCapacity(int[] ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.length - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
    max_capacity.cs
    /* Max capacity: Greedy algorithm */\nint MaxCapacity(int[] ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.Length - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = Math.Min(ht[i], ht[j]) * (j - i);\n        res = Math.Max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
    max_capacity.go
    /* Max capacity: Greedy algorithm */\nfunc maxCapacity(ht []int) int {\n    // Initialize i, j to be at both ends of the array\n    i, j := 0, len(ht)-1\n    // Initial max capacity is 0\n    res := 0\n    // Loop for greedy selection until the two boards meet\n    for i < j {\n        // Update max capacity\n        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n        res = int(math.Max(float64(res), float64(capacity)))\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
    max_capacity.swift
    /* Max capacity: Greedy algorithm */\nfunc maxCapacity(ht: [Int]) -> Int {\n    // Initialize i, j to be at both ends of the array\n    var i = ht.startIndex, j = ht.endIndex - 1\n    // Initial max capacity is 0\n    var res = 0\n    // Loop for greedy selection until the two boards meet\n    while i < j {\n        // Update max capacity\n        let cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i += 1\n        } else {\n            j -= 1\n        }\n    }\n    return res\n}\n
    max_capacity.js
    /* Max capacity: Greedy algorithm */\nfunction maxCapacity(ht) {\n    // Initialize i, j to be at both ends of the array\n    let i = 0,\n        j = ht.length - 1;\n    // Initial max capacity is 0\n    let res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        const cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
    max_capacity.ts
    /* Max capacity: Greedy algorithm */\nfunction maxCapacity(ht: number[]): number {\n    // Initialize i, j to be at both ends of the array\n    let i = 0,\n        j = ht.length - 1;\n    // Initial max capacity is 0\n    let res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
    max_capacity.dart
    /* Max capacity: Greedy algorithm */\nint maxCapacity(List<int> ht) {\n  // Initialize i, j to be at both ends of the array\n  int i = 0, j = ht.length - 1;\n  // Initial max capacity is 0\n  int res = 0;\n  // Loop for greedy selection until the two boards meet\n  while (i < j) {\n    // Update max capacity\n    int cap = min(ht[i], ht[j]) * (j - i);\n    res = max(res, cap);\n    // Move the shorter board inward\n    if (ht[i] < ht[j]) {\n      i++;\n    } else {\n      j--;\n    }\n  }\n  return res;\n}\n
    max_capacity.rs
    /* Max capacity: Greedy algorithm */\nfn max_capacity(ht: &[i32]) -> i32 {\n    // Initialize i, j to be at both ends of the array\n    let mut i = 0;\n    let mut j = ht.len() - 1;\n    // Initial max capacity is 0\n    let mut res = 0;\n    // Loop for greedy selection until the two boards meet\n    while i < j {\n        // Update max capacity\n        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n        res = std::cmp::max(res, cap);\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    res\n}\n
    max_capacity.c
    /* Max capacity: Greedy algorithm */\nint maxCapacity(int ht[], int htLength) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0;\n    int j = htLength - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int capacity = myMin(ht[i], ht[j]) * (j - i);\n        res = myMax(res, capacity);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
    max_capacity.kt
    /* Max capacity: Greedy algorithm */\nfun maxCapacity(ht: IntArray): Int {\n    // Initialize i, j to be at both ends of the array\n    var i = 0\n    var j = ht.size - 1\n    // Initial max capacity is 0\n    var res = 0\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        val cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
    max_capacity.rb
    ### Maximum capacity: greedy ###\ndef max_capacity(ht)\n  # Initialize i, j to be at both ends of the array\n  i, j = 0, ht.length - 1\n  # Initial max capacity is 0\n  res = 0\n\n  # Loop for greedy selection until the two boards meet\n  while i < j\n    # Update max capacity\n    cap = [ht[i], ht[j]].min * (j - i)\n    res = [res, cap].max\n    # Move the shorter board inward\n    if ht[i] < ht[j]\n      i += 1\n    else\n      j -= 1\n    end\n  end\n\n  res\nend\n
    ","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

    The reason greedy is faster than exhaustive enumeration is that each round of greedy selection \"skips\" some states.

    For example, in state \\(cap[i, j]\\), suppose \\(i\\) is the shorter partition and \\(j\\) is the taller partition. If we greedily move the shorter partition \\(i\\) inward by one position, the states shown in Figure 15-12 will be \"skipped.\" This means that their capacities can no longer be checked later.

    \\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\]

    Figure 15-12   States skipped by moving the short partition

    A closer look shows that these skipped states are exactly the states obtained by moving the taller partition \\(j\\) inward. We have already proven that moving the taller partition inward will definitely decrease the capacity. Therefore, none of the skipped states can be the optimal solution, so skipping them does not cause us to miss the optimum.

    The above analysis shows that moving the shorter partition is a \"safe\" operation, and that the greedy strategy is effective.

    ","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   Maximum Product Cutting Problem","text":"

    Question

    Given a positive integer \\(n\\), split it into the sum of at least two positive integers and find the maximum product of the resulting integers, as shown in Figure 15-13.

    Figure 15-13   Problem definition of max product cutting

    Suppose we split \\(n\\) into \\(m\\) integer factors, where the \\(i\\)-th factor is denoted as \\(n_i\\), that is

    \\[ n = \\sum_{i=1}^{m}n_i \\]

    The goal of this problem is to find the maximum product of all integer factors, namely

    \\[ \\max(\\prod_{i=1}^{m}n_i) \\]

    We need to determine how many parts \\(m\\) there should be and what each \\(n_i\\) should be.

    ","path":["Chapter 15. Greedy","15.4   Maximum Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1-determining-the-greedy-strategy","level":3,"title":"1.   Determining the Greedy Strategy","text":"

    As a rule of thumb, the product of two integers is often greater than their sum. Suppose we split off a factor of \\(2\\) from \\(n\\); the resulting product is \\(2(n-2)\\). We compare this product with \\(n\\):

    \\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

    As shown in Figure 15-14, when \\(n \\geq 4\\), splitting out a \\(2\\) will increase the product, which indicates that integers greater than or equal to \\(4\\) should all be split.

    Greedy strategy one: If the splitting scheme contains a factor \\(\\geq 4\\), it should be split further. The final splitting scheme should contain only the factors \\(1\\), \\(2\\), and \\(3\\).

    Figure 15-14   Splitting causes product to increase

    Next, consider which factor is optimal. Among the three factors \\(1\\), \\(2\\), and \\(3\\), clearly \\(1\\) is the worst, because \\(1 \\times (n-1) < n\\) always holds, meaning splitting out \\(1\\) will actually decrease the product.

    As shown in Figure 15-15, when \\(n = 6\\), we have \\(3 \\times 3 > 2 \\times 2 \\times 2\\). This means that splitting out \\(3\\) is better than splitting out \\(2\\).

    Greedy strategy two: In the splitting scheme, there should be at most two \\(2\\)s, because three \\(2\\)s can always be replaced by two \\(3\\)s to obtain a larger product.

    Figure 15-15   Optimal splitting factor

    In summary, the following greedy strategies can be derived.

    1. Input integer \\(n\\), continuously split out factor \\(3\\) until the remainder is \\(0\\), \\(1\\), or \\(2\\).
    2. When the remainder is \\(0\\), it means \\(n\\) is a multiple of \\(3\\), so no further action is needed.
    3. When the remainder is \\(2\\), do not split it further; keep it as is.
    4. When the remainder is \\(1\\), since \\(2 \\times 2 > 1 \\times 3\\), replace the final \\(3\\) and the remaining \\(1\\) with two \\(2\\)s.
    ","path":["Chapter 15. Greedy","15.4   Maximum Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

    As shown in Figure 15-16, we do not need loops to split the integer. Instead, we use integer division to obtain the number of \\(3\\)s, denoted by \\(a\\), and the modulo operation to obtain the remainder \\(b\\), giving:

    \\[ n = 3 a + b \\]

    Please note that for the edge case of \\(n \\leq 3\\), a \\(1\\) must be split out, with product \\(1 \\times (n - 1)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py
    def max_product_cutting(n: int) -> int:\n    \"\"\"Max product cutting: Greedy algorithm\"\"\"\n    # When n <= 3, must cut out a 1\n    if n <= 3:\n        return 1 * (n - 1)\n    # Greedily cut out 3, a is the number of 3s, b is the remainder\n    a, b = n // 3, n % 3\n    if b == 1:\n        # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return int(math.pow(3, a - 1)) * 2 * 2\n    if b == 2:\n        # When the remainder is 2, do nothing\n        return int(math.pow(3, a)) * 2\n    # When the remainder is 0, do nothing\n    return int(math.pow(3, a))\n
    max_product_cutting.cpp
    /* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int)pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int)pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int)pow(3, a);\n}\n
    max_product_cutting.java
    /* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int) Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int) Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int) Math.pow(3, a);\n}\n
    max_product_cutting.cs
    /* Max product cutting: Greedy algorithm */\nint MaxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int)Math.Pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int)Math.Pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int)Math.Pow(3, a);\n}\n
    max_product_cutting.go
    /* Max product cutting: Greedy algorithm */\nfunc maxProductCutting(n int) int {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    a := n / 3\n    b := n % 3\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return int(math.Pow(3, float64(a-1))) * 2 * 2\n    }\n    if b == 2 {\n        // When the remainder is 2, do nothing\n        return int(math.Pow(3, float64(a))) * 2\n    }\n    // When the remainder is 0, do nothing\n    return int(math.Pow(3, float64(a)))\n}\n
    max_product_cutting.swift
    /* Max product cutting: Greedy algorithm */\nfunc maxProductCutting(n: Int) -> Int {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = n / 3\n    let b = n % 3\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return pow(3, a - 1) * 2 * 2\n    }\n    if b == 2 {\n        // When the remainder is 2, do nothing\n        return pow(3, a) * 2\n    }\n    // When the remainder is 0, do nothing\n    return pow(3, a)\n}\n
    max_product_cutting.js
    /* Max product cutting: Greedy algorithm */\nfunction maxProductCutting(n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = Math.floor(n / 3);\n    let b = n % 3;\n    if (b === 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // When the remainder is 2, do nothing\n        return Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return Math.pow(3, a);\n}\n
    max_product_cutting.ts
    /* Max product cutting: Greedy algorithm */\nfunction maxProductCutting(n: number): number {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a: number = Math.floor(n / 3);\n    let b: number = n % 3;\n    if (b === 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // When the remainder is 2, do nothing\n        return Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return Math.pow(3, a);\n}\n
    max_product_cutting.dart
    /* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n  // When n <= 3, must cut out a 1\n  if (n <= 3) {\n    return 1 * (n - 1);\n  }\n  // Greedily cut out 3, a is the number of 3s, b is the remainder\n  int a = n ~/ 3;\n  int b = n % 3;\n  if (b == 1) {\n    // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n    return (pow(3, a - 1) * 2 * 2).toInt();\n  }\n  if (b == 2) {\n    // When the remainder is 2, do nothing\n    return (pow(3, a) * 2).toInt();\n  }\n  // When the remainder is 0, do nothing\n  return pow(3, a).toInt();\n}\n
    max_product_cutting.rs
    /* Max product cutting: Greedy algorithm */\nfn max_product_cutting(n: i32) -> i32 {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = n / 3;\n    let b = n % 3;\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        3_i32.pow(a as u32 - 1) * 2 * 2\n    } else if b == 2 {\n        // When the remainder is 2, do nothing\n        3_i32.pow(a as u32) * 2\n    } else {\n        // When the remainder is 0, do nothing\n        3_i32.pow(a as u32)\n    }\n}\n
    max_product_cutting.c
    /* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return pow(3, a);\n}\n
    max_product_cutting.kt
    /* Max product cutting: Greedy algorithm */\nfun maxProductCutting(n: Int): Int {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    val a = n / 3\n    val b = n % 3\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return 3.0.pow((a - 1)).toInt() * 2 * 2\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return 3.0.pow(a).toInt() * 2 * 2\n    }\n    // When the remainder is 0, do nothing\n    return 3.0.pow(a).toInt()\n}\n
    max_product_cutting.rb
    ### Maximum cutting product: greedy ###\ndef max_product_cutting(n)\n  # When n <= 3, must cut out a 1\n  return 1 * (n - 1) if n <= 3\n  # Greedily cut out 3, a is the number of 3s, b is the remainder\n  a, b = n / 3, n % 3\n  # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n  return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n  # When the remainder is 2, do nothing\n  return (3.pow(a) * 2).to_i if b == 2\n  # When the remainder is 0, do nothing\n  3.pow(a).to_i\nend\n

    Figure 15-16   Calculation method for max product cutting

    The time complexity depends on how exponentiation is implemented in the programming language. Taking Python as an example, there are three commonly used ways to compute powers.

    • Both the operator ** and the function pow() have time complexity \\(O(\\log⁡ a)\\).
    • The function math.pow() internally calls the C library's pow() function, which performs floating-point exponentiation, with time complexity \\(O(1)\\).

    Variables \\(a\\) and \\(b\\) use a constant amount of extra space, therefore the space complexity is \\(O(1)\\).

    ","path":["Chapter 15. Greedy","15.4   Maximum Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

    We use proof by contradiction and consider only the case where \\(n \\geq 4\\).

    1. All factors \\(\\leq 3\\): Suppose the optimal splitting scheme includes a factor \\(x \\geq 4\\). Then it can be further split into \\(2(x-2)\\) to obtain a larger (or equal) product. This contradicts the assumption.
    2. The splitting scheme does not contain \\(1\\): Suppose the optimal splitting scheme includes a factor of \\(1\\). Then it can be merged into another factor to obtain a larger product. This contradicts the assumption.
    3. The splitting scheme contains at most two \\(2\\)s: Suppose the optimal splitting scheme includes three \\(2\\)s. Then they can be replaced by two \\(3\\)s, yielding a larger product. This contradicts the assumption.
    ","path":["Chapter 15. Greedy","15.4   Maximum Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   Summary","text":"","path":["Chapter 15. Greedy","15.5   Summary"],"tags":[]},{"location":"chapter_greedy/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Greedy algorithms are typically used to solve optimization problems. The principle is to make locally optimal decisions at each decision stage in hopes of obtaining a globally optimal solution.
    • Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller subproblem in each round, until the problem is solved.
    • Greedy algorithms are not only simple to implement, but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms typically have lower time complexity.
    • In the coin change problem, for certain coin combinations, greedy algorithms can guarantee finding the optimal solution; for other coin combinations, however, greedy algorithms may find very poor solutions.
    • Problems suitable for solving with greedy algorithms have two major properties: greedy choice property and optimal substructure. The greedy choice property represents the effectiveness of the greedy strategy.
    • For some complex problems, proving the greedy choice property is not simple. Relatively speaking, disproving it is easier, such as in the coin change problem.
    • Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the main difficulty.
    • The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting fractions of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction.
    • The max capacity problem can be solved using exhaustive enumeration with time complexity \\(O(n^2)\\). By designing a greedy strategy to move the shorter side inward in each round, the time complexity can be optimized to \\(O(n)\\).
    • In the max product cutting problem, we successively derive two greedy strategies: integers \\(\\geq 4\\) should all continue to be split, and the optimal splitting factor is \\(3\\). The code includes exponentiation operations, and the time complexity depends on the implementation method of exponentiation, typically being \\(O(1)\\) or \\(O(\\log n)\\).
    ","path":["Chapter 15. Greedy","15.5   Summary"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"Chapter 6.   Hashing","text":"

    Abstract

    In the world of computing, a hash table is like a clever librarian.

    It knows how to compute call numbers, allowing it to quickly locate the desired book.

    ","path":["Chapter 6. Hashing","Chapter 6.   Hashing"],"tags":[]},{"location":"chapter_hashing/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 6.1   Hash Table
    • 6.2   Hash Collision
    • 6.3   Hash Algorithm
    • 6.4   Summary
    ","path":["Chapter 6. Hashing","Chapter 6.   Hashing"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   Hash Algorithm","text":"

    The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and separate chaining can only ensure that the hash table functions normally when hash collisions occur, but cannot reduce the frequency of hash collisions.

    If hash collisions occur too frequently, the performance of the hash table will deteriorate drastically. As shown in Figure 6-8, for a separate chaining hash table, in the ideal case, the key-value pairs are evenly distributed across the buckets, achieving optimal query efficiency; in the worst case, all key-value pairs are stored in the same bucket, degrading the time complexity to \\(O(n)\\).

    Figure 6-8   Ideal and worst cases of hash collisions

    The distribution of key-value pairs is determined by the hash function. Recall the steps of the hash function: first compute the hash value, then take it modulo the array length:

    index = hash(key) % capacity\n

    Observing the above formula, when the hash table capacity capacity is fixed, the hash algorithm hash() determines the output value, thereby determining the distribution of key-value pairs in the hash table.

    This means that, to reduce the probability of hash collisions, we should focus on the design of the hash algorithm hash().

    ","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631-goals-of-hash-algorithms","level":2,"title":"6.3.1   Goals of Hash Algorithms","text":"

    To build a hash table that is both fast and robust, a hash algorithm should have the following properties:

    • Determinism: For the same input, the hash algorithm should always produce the same output. Only then can the hash table be reliable.
    • High efficiency: The process of computing the hash value should be fast enough. The smaller the computational overhead, the more practical the hash table.
    • Uniform distribution: The hash algorithm should ensure that key-value pairs are evenly distributed in the hash table. The more uniform the distribution, the lower the probability of hash collisions.

    In fact, hash algorithms are not only used to implement hash tables but are also widely applied in other fields.

    • Password storage: To protect the security of user passwords, systems usually do not store the plaintext passwords but rather the hash values of the passwords. When a user enters a password, the system calculates the hash value of the input and compares it with the stored hash value. If they match, the password is considered correct.
    • Data integrity check: The data sender can calculate the hash value of the data and send it along; the receiver can recalculate the hash value of the received data and compare it with the received hash value. If they match, the data is considered intact.

    For cryptographic applications, hash algorithms need stronger security properties to prevent reverse engineering, such as inferring the original password from a hash value.

    • Unidirectionality: It should be impossible to deduce any information about the input data from the hash value.
    • Collision resistance: It should be extremely difficult to find two different inputs that produce the same hash value.
    • Avalanche effect: Minor changes in the input should lead to significant and unpredictable changes in the output.

    Note that \"uniform distribution\" and \"collision resistance\" are two independent concepts. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input key, the hash function key % 100 can produce a uniformly distributed output. However, this hash algorithm is too simple, and all key with the same last two digits will have the same output, making it easy to deduce a usable key from the hash value, thereby cracking the password.

    ","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632-design-of-hash-algorithms","level":2,"title":"6.3.2   Design of Hash Algorithms","text":"

    The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms.

    • Additive hash: Add up the ASCII codes of each character in the input and use the total sum as the hash value.
    • Multiplicative hash: Leverage the low correlation introduced by multiplication: multiply by a constant at each step and accumulate the ASCII codes of the characters into the hash value.
    • XOR hash: Accumulate the hash value by XORing each element of the input data.
    • Rotating hash: Accumulate the ASCII code of each character into a hash value, performing a rotation operation on the hash value before each accumulation.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py
    def add_hash(key: str) -> int:\n    \"\"\"Additive hash\"\"\"\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    \"\"\"Multiplicative hash\"\"\"\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 hash\"\"\"\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    \"\"\"Rotational hash\"\"\"\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    /* Additive hash */\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/* Multiplicative hash */\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 hash */\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/* Rotational hash */\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
    ### Additive hash ###\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### Multiplicative hash ###\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 hash ###\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### Rotational hash ###\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

    We can observe that the final step of each hash algorithm is to take the result modulo the large prime \\(1000000007\\), ensuring that the hash value stays within a suitable range. This naturally raises a question: why emphasize using a prime modulus, and what are the drawbacks of using a composite modulus?

    In short: using a large prime as the modulus helps maximize the uniformity of hash values. Because a prime shares no common factors with other numbers, it can reduce periodic patterns introduced by the modulo operation and thus mitigate hash collisions.

    For example, suppose we choose the composite number \\(9\\) as the modulus, which can be divided by \\(3\\), then all key divisible by \\(3\\) will be mapped to hash values \\(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} \\]

    If the input key values happen to follow this kind of arithmetic progression, the hash values will cluster, worsening hash collisions. Now suppose we replace modulus with the prime number \\(13\\). Because key and modulus share no common factors, the output hash values become much more evenly distributed.

    \\[ \\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} \\]

    It is worth noting that if the key is guaranteed to be randomly and uniformly distributed, then choosing a prime number or a composite number as the modulus can both produce uniformly distributed hash values. However, when the distribution of key has some periodicity, modulo a composite number is more likely to result in clustering.

    In summary, we usually choose a prime number as the modulus, and this prime number should be large enough to eliminate periodic patterns as much as possible, enhancing the robustness of the hash algorithm.

    ","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633-common-hash-algorithms","level":2,"title":"6.3.3   Common Hash Algorithms","text":"

    It is easy to see that the simple hash algorithms introduced above are fairly \"fragile\" and fall far short of the design goals of hash algorithms. For example, because addition and XOR are commutative, additive hash and XOR hash cannot distinguish strings with the same characters in a different order, which may worsen hash collisions and introduce security risks.

    In practice, we usually use some standard hash algorithms, such as MD5, SHA-1, SHA-2, and SHA-3. They can map input data of any length to a fixed-length hash value.

    Over the past century, hash algorithms have been in a continuous process of upgrading and optimization. Some researchers strive to improve the performance of hash algorithms, while others, including hackers, are dedicated to finding security issues in hash algorithms. Table 6-2 shows hash algorithms commonly used in practical applications.

    • MD5 and SHA-1 have been successfully attacked multiple times and are thus abandoned in various security applications.
    • SHA-2 series, especially SHA-256, is one of the most secure hash algorithms to date, with no successful attacks reported, hence commonly used in various security applications and protocols.
    • SHA-3 has lower implementation costs and higher computational efficiency compared to SHA-2, but its current usage coverage is not as extensive as the SHA-2 series.

    Table 6-2   Common hash algorithms

    MD5 SHA-1 SHA-2 SHA-3 Release Year 1992 1995 2002 2008 Output Length 128 bit 160 bit 256/512 bit 224/256/384/512 bit Hash Collisions Frequent Frequent Rare Rare Security Level Low, has been successfully attacked Low, has been successfully attacked High High Applications Abandoned, still used for data integrity checks Abandoned Cryptocurrency transaction verification, digital signatures, etc. Can be used to replace SHA-2","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#634-hash-values-in-data-structures","level":2,"title":"6.3.4   Hash Values in Data Structures","text":"

    We know that hash table keys can be integers, floating-point numbers, strings, and other data types. Programming languages usually provide built-in hash algorithms for these types to compute bucket indices in a hash table. Taking Python as an example, we can call the hash() function to compute hash values for various data types.

    • The hash values of integers and booleans are their own values.
    • The calculation of hash values for floating-point numbers and strings is more complex, and interested readers are encouraged to study this on their own.
    • The hash value of a tuple is obtained by hashing each of its elements and combining those results into a single hash value.
    • An object's hash value is typically generated from its memory address. By overriding the object's hash method, it can instead be generated from the object's contents.

    Tip

    Be aware that the definition and methods of the built-in hash value calculation functions in different programming languages vary.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py
    num = 3\nhash_num = hash(num)\n# Hash value of integer 3 is 3\n\nbol = True\nhash_bol = hash(bol)\n# Hash value of boolean True is 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# Hash value of decimal 3.14159 is 326484311674566659\n\nstr = \"Hello 算法\"\nhash_str = hash(str)\n# Hash value of string \"Hello 算法\" is 4617003410720528961\n\ntup = (12836, \"小哈\")\nhash_tup = hash(tup)\n# Hash value of tuple (12836, '小哈') is 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# Hash value of ListNode object at 0x1058fd810 is 274267521\n
    built_in_hash.cpp
    int num = 3;\nsize_t hashNum = hash<int>()(num);\n// Hash value of integer 3 is 3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// Hash value of boolean 1 is 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// Hash value of decimal 3.14159 is 4614256650576692846\n\nstring str = \"Hello 算法\";\nsize_t hashStr = hash<string>()(str);\n// Hash value of string \"Hello 算法\" is 15466937326284535026\n\n// In C++, built-in std::hash() only provides hash values for basic data types\n// Hash values for arrays and objects need to be implemented separately\n
    built_in_hash.java
    int num = 3;\nint hashNum = Integer.hashCode(num);\n// Hash value of integer 3 is 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// Hash value of boolean true is 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// Hash value of decimal 3.14159 is -1340954729\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode();\n// Hash value of string \"Hello 算法\" is -727081396\n\nObject[] arr = { 12836, \"小哈\" };\nint hashTup = Arrays.hashCode(arr);\n// Hash value of array [12836, 小哈] is 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// Hash value of ListNode object utils.ListNode@7dc5e7b4 is 2110121908\n
    built_in_hash.cs
    int num = 3;\nint hashNum = num.GetHashCode();\n// Hash value of integer 3 is 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// Hash value of boolean true is 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// Hash value of decimal 3.14159 is -1340954729;\n\nstring str = \"Hello 算法\";\nint hashStr = str.GetHashCode();\n// Hash value of string \"Hello 算法\" is -586107568;\n\nobject[] arr = [12836, \"小哈\"];\nint hashTup = arr.GetHashCode();\n// Hash value of array [12836, 小哈] is 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// Hash value of ListNode object 0 is 39053774;\n
    built_in_hash.go
    // Go does not provide built-in hash code functions\n
    built_in_hash.swift
    let num = 3\nlet hashNum = num.hashValue\n// Hash value of integer 3 is 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// Hash value of boolean true is -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// Hash value of decimal 3.14159 is -2465384235396674631\n\nlet str = \"Hello 算法\"\nlet hashStr = str.hashValue\n// Hash value of string \"Hello 算法\" is -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"小哈\")]\nlet hashTup = arr.hashValue\n// Hash value of array [AnyHashable(12836), AnyHashable(\"小哈\")] is -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// Hash value of ListNode object utils.ListNode is -2434780518035996159\n
    built_in_hash.js
    // JavaScript does not provide built-in hash code functions\n
    built_in_hash.ts
    // TypeScript does not provide built-in hash code functions\n
    built_in_hash.dart
    int num = 3;\nint hashNum = num.hashCode;\n// Hash value of integer 3 is 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// Hash value of boolean true is 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// Hash value of decimal 3.14159 is 2570631074981783\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode;\n// Hash value of string \"Hello 算法\" is 468167534\n\nList arr = [12836, \"小哈\"];\nint hashArr = arr.hashCode;\n// Hash value of array [12836, 小哈] is 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// Hash value of ListNode object Instance of 'ListNode' is 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// Hash value of integer 3 is 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// Hash value of boolean true is 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// Hash value of decimal 3.14159 is 2566941990314602357\n\nlet str = \"Hello 算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// Hash value of string \"Hello 算法\" is 16092673739211250988\n\nlet arr = (&12836, &\"小哈\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// Hash value of tuple (12836, \"小哈\") is 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// Hash value of ListNode object RefCell { value: ListNode { val: 42, next: None } } is 15387811073369036852\n
    built_in_hash.c
    // C does not provide built-in hash code functions\n
    built_in_hash.kt
    val num = 3\nval hashNum = num.hashCode()\n// Hash value of integer 3 is 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// Hash value of boolean true is 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// Hash value of decimal 3.14159 is -1340954729\n\nval str = \"Hello 算法\"\nval hashStr = str.hashCode()\n// Hash value of string \"Hello 算法\" is -727081396\n\nval arr = arrayOf<Any>(12836, \"小哈\")\nval hashTup = arr.hashCode()\n// Hash value of array [12836, 小哈] is 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// Hash value of ListNode object utils.ListNode@1d81eb93 is 495053715\n
    built_in_hash.rb
    num = 3\nhash_num = num.hash\n# Hash value of integer 3 is -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# Hash value of boolean true is -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# Hash value of decimal 3.14159 is -1479186995943067893\n\nstr = \"Hello 算法\"\nhash_str = str.hash\n# Hash value of string \"Hello 算法\" is -4075943250025831763\n\ntup = [12836, '小哈']\nhash_tup = tup.hash\n# Hash value of tuple (12836, '小哈') is 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# Hash value of ListNode object #<ListNode:0x000078133140ab70> is 4302940560806366381\n
    Visualized Execution

    https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

    In many programming languages, only immutable objects can serve as the key in a hash table. If we use a list (dynamic array) as a key, when the contents of the list change, its hash value also changes, and we would no longer be able to find the original value in the hash table.

    Although the member variables of a custom object (such as a linked list node) are mutable, it is hashable. This is because the hash value of an object is usually generated based on its memory address, and even if the contents of the object change, the memory address remains the same, so the hash value remains unchanged.

    You might have noticed that the hash values output in different consoles are different. This is because the Python interpreter adds a random salt to the string hash function each time it starts up. This approach effectively prevents HashDoS attacks and enhances the security of the hash algorithm.

    ","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   Hash Collision","text":"

    The previous section mentioned that, in most cases, the input space of a hash function is much larger than the output space, so theoretically, hash collisions are inevitable. For example, if the input space is all integers and the output space is the array capacity size, then multiple integers will inevitably be mapped to the same bucket index.

    Hash collisions can lead to incorrect query results, severely impacting the usability of the hash table. To address this issue, whenever a hash collision occurs, we can perform hash table expansion until the collision disappears. This approach is simple, straightforward, and effective, but it is very inefficient because hash table expansion involves a large amount of data migration and hash value recalculation. To improve efficiency, we can adopt the following strategies:

    1. Improve the hash table data structure so that the hash table can function normally when hash collisions occur.
    2. Only expand when necessary, that is, only when hash collisions are severe.

    The main approaches to improving a hash table's structure are separate chaining and open addressing.

    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621-separate-chaining","level":2,"title":"6.2.1   Separate Chaining","text":"

    In the original hash table, each bucket can store only one key-value pair. Separate chaining replaces the single element in each bucket with a linked list, treating each key-value pair as a node and storing all colliding key-value pairs in the same list. Figure 6-5 shows an example of a separate chaining hash table.

    Figure 6-5   Separate chaining hash table

    In a hash table implemented with separate chaining, the basic operations work as follows:

    • Querying elements: Input key, compute the bucket index using the hash function, access the head of the corresponding linked list, and traverse the list while comparing keys until the target key-value pair is found.
    • Adding elements: First use the hash function to locate the corresponding linked list, then insert the node (key-value pair) into the list.
    • Deleting elements: Use the hash function to locate the corresponding linked list, then traverse it to find and delete the target node.

    Separate chaining has the following limitations:

    • Increased Space Usage: The linked list contains node pointers, which consume more memory space than arrays.
    • Reduced Query Efficiency: This is because linear traversal of the linked list is required to find the corresponding element.

    The code below provides a simple implementation of a separate chaining hash table, with two things to note:

    • Lists (dynamic arrays) are used instead of linked lists to simplify the code. In this setup, the hash table (array) contains multiple buckets, each of which is a list.
    • This implementation includes a hash table expansion method. When the load factor exceeds \\(\\frac{2}{3}\\), we expand the hash table to \\(2\\) times its original size.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py
    class HashMapChaining:\n    \"\"\"Hash table with separate chaining\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self.size = 0  # Number of key-value pairs\n        self.capacity = 4  # Hash table capacity\n        self.load_thres = 2.0 / 3.0  # Load factor threshold for triggering expansion\n        self.extend_ratio = 2  # Expansion multiplier\n        self.buckets = [[] for _ in range(self.capacity)]  # Bucket array\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Load factor\"\"\"\n        return self.size / self.capacity\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Query operation\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket, if key is found, return corresponding val\n        for pair in bucket:\n            if pair.key == key:\n                return pair.val\n        # If key is not found, return None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Add operation\"\"\"\n        # When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in bucket:\n            if pair.key == key:\n                pair.val = val\n                return\n        # If key does not exist, append key-value pair to the end\n        pair = Pair(key, val)\n        bucket.append(pair)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket and remove key-value pair from it\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        \"\"\"Expand hash table\"\"\"\n        # Temporarily store the original hash table\n        buckets = self.buckets\n        # Initialize expanded new hash table\n        self.capacity *= self.extend_ratio\n        self.buckets = [[] for _ in range(self.capacity)]\n        self.size = 0\n        # Move key-value pairs from original hash table to new hash table\n        for bucket in buckets:\n            for pair in bucket:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Print hash table\"\"\"\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n  private:\n    int size;                       // Number of key-value pairs\n    int capacity;                   // Hash table capacity\n    double loadThres;               // Load factor threshold for triggering expansion\n    int extendRatio;                // Expansion multiplier\n    vector<vector<Pair *>> buckets; // Bucket array\n\n  public:\n    /* Constructor */\n    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n        buckets.resize(capacity);\n    }\n\n    /* Destructor */\n    ~HashMapChaining() {\n        for (auto &bucket : buckets) {\n            for (Pair *pair : bucket) {\n                // Free memory\n                delete pair;\n            }\n        }\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double)size / (double)capacity;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        int index = hashFunc(key);\n        // Traverse bucket, if key is found, return corresponding val\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                return pair->val;\n            }\n        }\n        // Return empty string if key not found\n        return \"\";\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                pair->val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        buckets[index].push_back(new Pair(key, val));\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        auto &bucket = buckets[index];\n        // Traverse bucket and remove key-value pair from it\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); // Remove key-value pair from it\n                delete tmp;                       // Free memory\n                size--;\n                return;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        vector<vector<Pair *>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets.clear();\n        buckets.resize(capacity);\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (auto &bucket : bucketsTmp) {\n            for (Pair *pair : bucket) {\n                put(pair->key, pair->val);\n                // Free memory\n                delete pair;\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    int size; // Number of key-value pairs\n    int capacity; // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio; // Expansion multiplier\n    List<List<Pair>> buckets; // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Query operation */\n    String get(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket, if key is found, return corresponding val\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    void put(int key, String val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        Pair pair = new Pair(key, val);\n        bucket.add(pair);\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket and remove key-value pair from it\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        List<List<Pair>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\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        // Move key-value pairs from original hash table to new hash table\n        for (List<Pair> bucket : bucketsTmp) {\n            for (Pair pair : bucket) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    int size; // Number of key-value pairs\n    int capacity; // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio; // Expansion multiplier\n    List<List<Pair>> buckets; // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        // Traverse bucket, if key is found, return corresponding val\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        int index = HashFunc(key);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        buckets[index].Add(new Pair(key, val));\n        size++;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Traverse bucket and remove key-value pair from it\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    /* Expand hash table */\n    void Extend() {\n        // Temporarily store the original hash table\n        List<List<Pair>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\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        // Move key-value pairs from original hash table to new hash table\n        foreach (List<Pair> bucket in bucketsTmp) {\n            foreach (Pair pair in bucket) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\ntype hashMapChaining struct {\n    size        int      // Number of key-value pairs\n    capacity    int      // Hash table capacity\n    loadThres   float64  // Load factor threshold for triggering expansion\n    extendRatio int      // Expansion multiplier\n    buckets     [][]pair // Bucket array\n}\n\n/* Constructor */\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/* Hash function */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n    return key % m.capacity\n}\n\n/* Load factor */\nfunc (m *hashMapChaining) loadFactor() float64 {\n    return float64(m.size) / float64(m.capacity)\n}\n\n/* Query operation */\nfunc (m *hashMapChaining) get(key int) string {\n    idx := m.hashFunc(key)\n    bucket := m.buckets[idx]\n    // Traverse bucket, if key is found, return corresponding val\n    for _, p := range bucket {\n        if p.key == key {\n            return p.val\n        }\n    }\n    // Return empty string if key not found\n    return \"\"\n}\n\n/* Add operation */\nfunc (m *hashMapChaining) put(key int, val string) {\n    // When load factor exceeds threshold, perform expansion\n    if m.loadFactor() > m.loadThres {\n        m.extend()\n    }\n    idx := m.hashFunc(key)\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\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    // If key does not exist, append key-value pair to the end\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/* Remove operation */\nfunc (m *hashMapChaining) remove(key int) {\n    idx := m.hashFunc(key)\n    // Traverse bucket and remove key-value pair from it\n    for i, p := range m.buckets[idx] {\n        if p.key == key {\n            // Slice deletion\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/* Expand hash table */\nfunc (m *hashMapChaining) extend() {\n    // Temporarily store the original hash table\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    // Initialize expanded new hash table\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    // Move key-value pairs from original hash table to new hash table\n    for _, bucket := range tmpBuckets {\n        for _, p := range bucket {\n            m.put(p.key, p.val)\n        }\n    }\n}\n\n/* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    var loadThres: Double // Load factor threshold for triggering expansion\n    var extendRatio: Int // Expansion multiplier\n    var buckets: [[Pair]] // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Load factor */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket, if key is found, return corresponding val\n        for pair in bucket {\n            if pair.key == key {\n                return pair.val\n            }\n        }\n        // Return nil if key not found\n        return nil\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if loadFactor() > loadThres {\n            extend()\n        }\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in bucket {\n            if pair.key == key {\n                pair.val = val\n                return\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        let pair = Pair(key: key, val: val)\n        buckets[index].append(pair)\n        size += 1\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket and remove key-value pair from it\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    /* Expand hash table */\n    func extend() {\n        // Temporarily store the original hash table\n        let bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = Array(repeating: [], count: capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for bucket in bucketsTmp {\n            for pair in bucket {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    #size; // Number of key-value pairs\n    #capacity; // Hash table capacity\n    #loadThres; // Load factor threshold for triggering expansion\n    #extendRatio; // Expansion multiplier\n    #buckets; // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Query operation */\n    get(key) {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if key is found, return corresponding val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key, val) {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key) {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Traverse bucket and remove key-value pair from it\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    /* Expand hash table */\n    #extend() {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    #size: number; // Number of key-value pairs\n    #capacity: number; // Hash table capacity\n    #loadThres: number; // Load factor threshold for triggering expansion\n    #extendRatio: number; // Expansion multiplier\n    #buckets: Pair[][]; // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    #hashFunc(key: number): number {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor(): number {\n        return this.#size / this.#capacity;\n    }\n\n    /* Query operation */\n    get(key: number): string | null {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if key is found, return corresponding val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key: number, val: string): void {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key: number): void {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Traverse bucket and remove key-value pair from it\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    /* Expand hash table */\n    #extend(): void {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n  late int size; // Number of key-value pairs\n  late int capacity; // Hash table capacity\n  late double loadThres; // Load factor threshold for triggering expansion\n  late int extendRatio; // Expansion multiplier\n  late List<List<Pair>> buckets; // Bucket array\n\n  /* Constructor */\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  /* Hash function */\n  int hashFunc(int key) {\n    return key % capacity;\n  }\n\n  /* Load factor */\n  double loadFactor() {\n    return size / capacity;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket, if key is found, return corresponding val\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        return pair.val;\n      }\n    }\n    // If key is not found, return null\n    return null;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor() > loadThres) {\n      extend();\n    }\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        pair.val = val;\n        return;\n      }\n    }\n    // If key does not exist, append key-value pair to the end\n    Pair pair = Pair(key, val);\n    bucket.add(pair);\n    size++;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket and remove key-value pair from it\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        bucket.remove(pair);\n        size--;\n        break;\n      }\n    }\n  }\n\n  /* Expand hash table */\n  void extend() {\n    // Temporarily store the original hash table\n    List<List<Pair>> bucketsTmp = buckets;\n    // Initialize expanded new hash table\n    capacity *= extendRatio;\n    buckets = List.generate(capacity, (_) => []);\n    size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (List<Pair> bucket in bucketsTmp) {\n      for (Pair pair in bucket) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Print hash table */\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
    /* Hash table with separate chaining */\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    /* Constructor */\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    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % self.capacity\n    }\n\n    /* Load factor */\n    fn load_factor(&self) -> f32 {\n        self.size as f32 / self.capacity as f32\n    }\n\n    /* Remove operation */\n    fn remove(&mut self, key: i32) -> Option<String> {\n        let index = self.hash_func(key);\n\n        // Traverse bucket and remove key-value pair from it\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        // If key is not found, return None\n        None\n    }\n\n    /* Expand hash table */\n    fn extend(&mut self) {\n        // Temporarily store the original hash table\n        let buckets_tmp = std::mem::take(&mut self.buckets);\n\n        // Initialize expanded new hash table\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![Vec::new(); self.capacity as usize];\n        self.size = 0;\n\n        // Move key-value pairs from original hash table to new hash table\n        for bucket in buckets_tmp {\n            for pair in bucket {\n                self.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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    /* Add operation */\n    fn put(&mut self, key: i32, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n\n        let index = self.hash_func(key);\n\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in self.buckets[index].iter_mut() {\n            if pair.key == key {\n                pair.val = val;\n                return;\n            }\n        }\n\n        // If key does not exist, append key-value pair to the end\n        let pair = Pair { key, val };\n        self.buckets[index].push(pair);\n        self.size += 1;\n    }\n\n    /* Query operation */\n    fn get(&self, key: i32) -> Option<&str> {\n        let index = self.hash_func(key);\n\n        // Traverse bucket, if key is found, return corresponding val\n        for pair in self.buckets[index].iter() {\n            if pair.key == key {\n                return Some(&pair.val);\n            }\n        }\n\n        // If key is not found, return None\n        None\n    }\n}\n
    hash_map_chaining.c
    /* Linked list node */\ntypedef struct Node {\n    Pair *pair;\n    struct Node *next;\n} Node;\n\n/* Hash table with separate chaining */\ntypedef struct {\n    int size;         // Number of key-value pairs\n    int capacity;     // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio;  // Expansion multiplier\n    Node **buckets;   // Bucket array\n} HashMapChaining;\n\n/* Constructor */\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/* Destructor */\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/* Hash function */\nint hashFunc(HashMapChaining *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Load factor */\ndouble loadFactor(HashMapChaining *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Query operation */\nchar *get(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    // Traverse bucket, if key is found, return corresponding 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 \"\"; // Return empty string if key not found\n}\n\n/* Add operation */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    int index = hashFunc(hashMap, key);\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            strcpy(cur->pair->val, val); // If specified key is found, update corresponding val and return\n            return;\n        }\n        cur = cur->next;\n    }\n    // If key not found, add key-value pair to list head\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/* Expand hash table */\nvoid extend(HashMapChaining *hashMap) {\n    // Temporarily store the original hash table\n    int oldCapacity = hashMap->capacity;\n    Node **oldBuckets = hashMap->buckets;\n    // Initialize expanded new hash table\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    // Move key-value pairs from original hash table to new hash table\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            // Free memory\n            free(temp->pair);\n            free(temp);\n        }\n    }\n\n    free(oldBuckets);\n}\n\n/* Remove operation */\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            // Remove key-value pair from it\n            if (pre) {\n                pre->next = cur->next;\n            } else {\n                hashMap->buckets[index] = cur->next;\n            }\n            // Free memory\n            free(cur->pair);\n            free(cur);\n            hashMap->size--;\n            return;\n        }\n        pre = cur;\n        cur = cur->next;\n    }\n}\n\n/* Print hash table */\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
    /* Hash table with separate chaining */\nclass HashMapChaining {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    val loadThres: Double // Load factor threshold for triggering expansion\n    val extendRatio: Int // Expansion multiplier\n    var buckets: MutableList<MutableList<Pair>> // Bucket array\n\n    /* Constructor */\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    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Load factor */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket, if key is found, return corresponding val\n        for (pair in bucket) {\n            if (pair.key == key) return pair._val\n        }\n        // If key is not found, return null\n        return null\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (pair in bucket) {\n            if (pair.key == key) {\n                pair._val = _val\n                return\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        val pair = Pair(key, _val)\n        bucket.add(pair)\n        size++\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket and remove key-value pair from it\n        for (pair in bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair)\n                size--\n                break\n            }\n        }\n    }\n\n    /* Expand hash table */\n    fun extend() {\n        // Temporarily store the original hash table\n        val bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        // mutablelist has no fixed size\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for (bucket in bucketsTmp) {\n            for (pair in bucket) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Print hash table */\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
    ### Hash map with chaining ###\nclass HashMapChaining\n  ### Constructor ###\n  def initialize\n    @size = 0 # Number of key-value pairs\n    @capacity = 4 # Hash table capacity\n    @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion\n    @extend_ratio = 2 # Expansion multiplier\n    @buckets = Array.new(@capacity) { [] } # Bucket array\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Load factor ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Query operation ###\n  def get(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket, if key is found, return corresponding val\n    for pair in bucket\n      return pair.val if pair.key == key\n    end\n    # Return nil if key not found\n    nil\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    # When load factor exceeds threshold, perform expansion\n    extend if load_factor > @load_thres\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket, if specified key is encountered, update corresponding val and return\n    for pair in bucket\n      if pair.key == key\n        pair.val = val\n        return\n      end\n    end\n    # If key does not exist, append key-value pair to the end\n    pair = Pair.new(key, val)\n    bucket << pair\n    @size += 1\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket and remove key-value pair from it\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  ### Expand hash table ###\n  def extend\n    # Temporarily store original hash table\n    buckets = @buckets\n    # Initialize expanded new hash table\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity) { [] }\n    @size = 0\n    # Move key-value pairs from original hash table to new hash table\n    for bucket in buckets\n      for pair in bucket\n        put(pair.key, pair.val)\n      end\n    end\n  end\n\n  ### Print hash table ###\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

    It's worth noting that when the linked list becomes very long, the query time \\(O(n)\\) is poor. In this case, the linked list can be converted into an AVL tree or a red-black tree, reducing the time complexity of lookups to \\(O(\\log n)\\).

    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622-open-addressing","level":2,"title":"6.2.2   Open Addressing","text":"

    Open addressing does not introduce additional data structures. Instead, it handles hash collisions through repeated probing. Common probing strategies include linear probing, quadratic probing, and multiple hashing.

    Let's use linear probing as an example to introduce the mechanism of open addressing hash tables.

    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1-linear-probing","level":3,"title":"1.   Linear Probing","text":"

    Linear probing uses a fixed step size to probe sequentially, so its operations differ somewhat from those of an ordinary hash table.

    • Inserting elements: Compute the bucket index using the hash function. If the bucket is already occupied, continue probing forward from the collision position with a fixed step size (usually \\(1\\)) until an empty bucket is found, then insert the element there.
    • Searching for elements: If a collision occurs, continue probing forward with the same step size until the corresponding element is found and return its value; if an empty bucket is encountered, the target element is not in the hash table, so return None.

    Figure 6-6 shows the distribution of key-value pairs in an open-addressing hash table that uses linear probing. Under this hash function, keys with the same last two digits are mapped to the same bucket. Linear probing then places them in that bucket and the subsequent buckets.

    Figure 6-6   Distribution of key-value pairs in open addressing (linear probing) hash table

    However, linear probing is prone to clustering. Specifically, the longer a contiguous occupied region in the array becomes, the more likely new collisions are to occur within that region. This in turn makes the cluster grow even further, creating a vicious cycle that gradually degrades the efficiency of insertion, deletion, lookup, and update operations.

    It's important to note that we cannot directly delete elements from an open-addressing hash table. Deleting an element creates an empty bucket None in the array. During lookup, once linear probing reaches that empty bucket, it stops, which means any elements stored farther along the probe sequence become unreachable. As a result, the program may incorrectly conclude that those elements do not exist, as shown in Figure 6-7.

    Figure 6-7   Query issues caused by deletion in open addressing

    To solve this problem, we can adopt lazy deletion: instead of directly removing an element from the hash table, use a constant TOMBSTONE to mark the bucket. Under this mechanism, both None and TOMBSTONE denote buckets that can accept key-value pairs. The difference is that when linear probing encounters TOMBSTONE, it must continue probing, because key-value pairs may still exist farther along the sequence.

    However, lazy deletion may accelerate hash-table performance degradation. Each deletion leaves behind a marker, and as the number of TOMBSTONE entries grows, search time increases as well, because linear probing may need to skip over multiple tombstones before finding the target element.

    To address this, we can record the index of the first TOMBSTONE encountered during linear probing and swap the found target element into that position. The benefit is that each query or insertion can move elements closer to their ideal positions, that is, closer to where probing begins, which improves lookup efficiency.

    The code below implements an open addressing (linear probing) hash table with lazy deletion. To make better use of the hash table space, we treat the hash table as a \"circular array\". When going beyond the end of the array, we return to the beginning and continue traversing.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py
    class HashMapOpenAddressing:\n    \"\"\"Hash table with open addressing\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self.size = 0  # Number of key-value pairs\n        self.capacity = 4  # Hash table capacity\n        self.load_thres = 2.0 / 3.0  # Load factor threshold for triggering expansion\n        self.extend_ratio = 2  # Expansion multiplier\n        self.buckets: list[Pair | None] = [None] * self.capacity  # Bucket array\n        self.TOMBSTONE = Pair(-1, \"-1\")  # Removal marker\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Load factor\"\"\"\n        return self.size / self.capacity\n\n    def find_bucket(self, key: int) -> int:\n        \"\"\"Search for bucket index corresponding to key\"\"\"\n        index = self.hash_func(key)\n        first_tombstone = -1\n        # Linear probing, break when encountering an empty bucket\n        while self.buckets[index] is not None:\n            # If key is encountered, return the corresponding bucket index\n            if self.buckets[index].key == key:\n                # If a removal marker was encountered before, move the key-value pair to that index\n                if first_tombstone != -1:\n                    self.buckets[first_tombstone] = self.buckets[index]\n                    self.buckets[index] = self.TOMBSTONE\n                    return first_tombstone  # Return the moved bucket index\n                return index  # Return bucket index\n            # Record the first removal marker encountered\n            if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n                first_tombstone = index\n            # Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % self.capacity\n        # If key does not exist, return the index for insertion\n        return index if first_tombstone == -1 else first_tombstone\n\n    def get(self, key: int) -> str:\n        \"\"\"Query operation\"\"\"\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, return corresponding val\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            return self.buckets[index].val\n        # If key-value pair does not exist, return None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Add operation\"\"\"\n        # When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, overwrite val and return\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index].val = val\n            return\n        # If key-value pair does not exist, add the key-value pair\n        self.buckets[index] = Pair(key, val)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, overwrite it with removal marker\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        \"\"\"Expand hash table\"\"\"\n        # Temporarily store the original hash table\n        buckets_tmp = self.buckets\n        # Initialize expanded new hash table\n        self.capacity *= self.extend_ratio\n        self.buckets = [None] * self.capacity\n        self.size = 0\n        # Move key-value pairs from original hash table to new hash table\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        \"\"\"Print hash table\"\"\"\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n  private:\n    int size;                             // Number of key-value pairs\n    int capacity = 4;                     // Hash table capacity\n    const double loadThres = 2.0 / 3.0;     // Load factor threshold for triggering expansion\n    const int extendRatio = 2;            // Expansion multiplier\n    vector<Pair *> buckets;               // Bucket array\n    Pair *TOMBSTONE = new Pair(-1, \"-1\"); // Removal marker\n\n  public:\n    /* Constructor */\n    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n    }\n\n    /* Destructor */\n    ~HashMapOpenAddressing() {\n        for (Pair *pair : buckets) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                delete pair;\n            }\n        }\n        delete TOMBSTONE;\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != nullptr) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index]->key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            return buckets[index]->val;\n        }\n        // Return empty string if key-value pair does not exist\n        return \"\";\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            buckets[index]->val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            delete buckets[index];\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        vector<Pair *> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = vector<Pair *>(capacity, nullptr);\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\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    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private int size; // Number of key-value pairs\n    private int capacity = 4; // Hash table capacity\n    private final double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n    private final int extendRatio = 2; // Expansion multiplier\n    private Pair[] buckets; // Bucket array\n    private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // Removal marker\n\n    /* Constructor */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Hash function */\n    private int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    private double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    private int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index].key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    public String get(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void put(int key, String val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    public void remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    private void extend() {\n        // Temporarily store the original hash table\n        Pair[] bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (Pair pair : bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    int size; // Number of key-value pairs\n    int capacity = 4; // Hash table capacity\n    double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n    int extendRatio = 2; // Expansion multiplier\n    Pair[] buckets; // Bucket array\n    Pair TOMBSTONE = new(-1, \"-1\"); // Removal marker\n\n    /* Constructor */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    int FindBucket(int key) {\n        int index = HashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index].key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    void Extend() {\n        // Temporarily store the original hash table\n        Pair[] bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        foreach (Pair pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with open addressing */\ntype hashMapOpenAddressing struct {\n    size        int     // Number of key-value pairs\n    capacity    int     // Hash table capacity\n    loadThres   float64 // Load factor threshold for triggering expansion\n    extendRatio int     // Expansion multiplier\n    buckets     []*pair // Bucket array\n    TOMBSTONE   *pair   // Removal marker\n}\n\n/* Constructor */\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/* Hash function */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n    return key % h.capacity // Calculate hash value based on key\n}\n\n/* Load factor */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n    return float64(h.size) / float64(h.capacity) // Calculate current load factor\n}\n\n/* Search for bucket index corresponding to key */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n    index := h.hashFunc(key) // Get initial index\n    firstTombstone := -1     // Record position of first TOMBSTONE encountered\n    for h.buckets[index] != nil {\n        if h.buckets[index].key == key {\n            if firstTombstone != -1 {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                h.buckets[firstTombstone] = h.buckets[index]\n                h.buckets[index] = h.TOMBSTONE\n                return firstTombstone // Return the moved bucket index\n            }\n            return index // Return found index\n        }\n        if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE {\n            firstTombstone = index // Record position of first deletion marker encountered\n        }\n        index = (index + 1) % h.capacity // Linear probing, wrap around to head if past tail\n    }\n    // If key does not exist, return the index for insertion\n    if firstTombstone != -1 {\n        return firstTombstone\n    }\n    return index\n}\n\n/* Query operation */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        return h.buckets[index].val // If key-value pair is found, return corresponding val\n    }\n    return \"\" // Return \"\" if key-value pair does not exist\n}\n\n/* Add operation */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n    if h.loadFactor() > h.loadThres {\n        h.extend() // When load factor exceeds threshold, perform expansion\n    }\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n        h.buckets[index] = &pair{key, val} // If key-value pair does not exist, add the key-value pair\n        h.size++\n    } else {\n        h.buckets[index].val = val // If key-value pair found, overwrite val\n    }\n}\n\n/* Remove operation */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        h.buckets[index] = h.TOMBSTONE // If key-value pair is found, overwrite it with removal marker\n        h.size--\n    }\n}\n\n/* Expand hash table */\nfunc (h *hashMapOpenAddressing) extend() {\n    oldBuckets := h.buckets               // Temporarily store the original hash table\n    h.capacity *= h.extendRatio           // Update capacity\n    h.buckets = make([]*pair, h.capacity) // Initialize expanded new hash table\n    h.size = 0                            // Reset size\n    // Move key-value pairs from original hash table to new hash table\n    for _, pair := range oldBuckets {\n        if pair != nil && pair != h.TOMBSTONE {\n            h.put(pair.key, pair.val)\n        }\n    }\n}\n\n/* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    var loadThres: Double // Load factor threshold for triggering expansion\n    var extendRatio: Int // Expansion multiplier\n    var buckets: [Pair?] // Bucket array\n    var TOMBSTONE: Pair // Removal marker\n\n    /* Constructor */\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    /* Hash function */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Load factor */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Search for bucket index corresponding to key */\n    func findBucket(key: Int) -> Int {\n        var index = hashFunc(key: key)\n        var firstTombstone = -1\n        // Linear probing, break when encountering an empty bucket\n        while buckets[index] != nil {\n            // If key is encountered, return the corresponding bucket index\n            if buckets[index]!.key == key {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if firstTombstone != -1 {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Return the moved bucket index\n                }\n                return index // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if firstTombstone == -1 && buckets[index] == TOMBSTONE {\n                firstTombstone = index\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, return corresponding val\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            return buckets[index]!.val\n        }\n        // If key-value pair does not exist, return null\n        return nil\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if loadFactor() > loadThres {\n            extend()\n        }\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, overwrite val and return\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index]!.val = val\n            return\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = Pair(key: key, val: val)\n        size += 1\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, overwrite it with removal marker\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index] = TOMBSTONE\n            size -= 1\n        }\n    }\n\n    /* Expand hash table */\n    func extend() {\n        // Temporarily store the original hash table\n        let bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = Array(repeating: nil, count: capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for pair in bucketsTmp {\n            if let pair, pair != TOMBSTONE {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    #size; // Number of key-value pairs\n    #capacity; // Hash table capacity\n    #loadThres; // Load factor threshold for triggering expansion\n    #extendRatio; // Expansion multiplier\n    #buckets; // Bucket array\n    #TOMBSTONE; // Removal marker\n\n    /* Constructor */\n    constructor() {\n        this.#size = 0; // Number of key-value pairs\n        this.#capacity = 4; // Hash table capacity\n        this.#loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n        this.#extendRatio = 2; // Expansion multiplier\n        this.#buckets = Array(this.#capacity).fill(null); // Bucket array\n        this.#TOMBSTONE = new Pair(-1, '-1'); // Removal marker\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    #findBucket(key) {\n        let index = this.#hashFunc(key);\n        let firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (this.#buckets[index] !== null) {\n            // If key is encountered, return the corresponding bucket index\n            if (this.#buckets[index].key === key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone !== -1) {\n                    this.#buckets[firstTombstone] = this.#buckets[index];\n                    this.#buckets[index] = this.#TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (\n                firstTombstone === -1 &&\n                this.#buckets[index] === this.#TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % this.#capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    get(key) {\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            return this.#buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key, val) {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        this.#buckets[index] = new Pair(key, val);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key) {\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\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    /* Expand hash table */\n    #extend() {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = Array(this.#capacity).fill(null);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\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    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private size: number; // Number of key-value pairs\n    private capacity: number; // Hash table capacity\n    private loadThres: number; // Load factor threshold for triggering expansion\n    private extendRatio: number; // Expansion multiplier\n    private buckets: Array<Pair | null>; // Bucket array\n    private TOMBSTONE: Pair; // Removal marker\n\n    /* Constructor */\n    constructor() {\n        this.size = 0; // Number of key-value pairs\n        this.capacity = 4; // Hash table capacity\n        this.loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n        this.extendRatio = 2; // Expansion multiplier\n        this.buckets = Array(this.capacity).fill(null); // Bucket array\n        this.TOMBSTONE = new Pair(-1, '-1'); // Removal marker\n    }\n\n    /* Hash function */\n    private hashFunc(key: number): number {\n        return key % this.capacity;\n    }\n\n    /* Load factor */\n    private loadFactor(): number {\n        return this.size / this.capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    private findBucket(key: number): number {\n        let index = this.hashFunc(key);\n        let firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (this.buckets[index] !== null) {\n            // If key is encountered, return the corresponding bucket index\n            if (this.buckets[index]!.key === key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone !== -1) {\n                    this.buckets[firstTombstone] = this.buckets[index];\n                    this.buckets[index] = this.TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (\n                firstTombstone === -1 &&\n                this.buckets[index] === this.TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % this.capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    get(key: number): string | null {\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            return this.buckets[index]!.val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key: number, val: string): void {\n        // When load factor exceeds threshold, perform expansion\n        if (this.loadFactor() > this.loadThres) {\n            this.extend();\n        }\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index]!.val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        this.buckets[index] = new Pair(key, val);\n        this.size++;\n    }\n\n    /* Remove operation */\n    remove(key: number): void {\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\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    /* Expand hash table */\n    private extend(): void {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.buckets;\n        // Initialize expanded new hash table\n        this.capacity *= this.extendRatio;\n        this.buckets = Array(this.capacity).fill(null);\n        this.size = 0;\n        // Move key-value pairs from original hash table to new hash table\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    /* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n  late int _size; // Number of key-value pairs\n  int _capacity = 4; // Hash table capacity\n  double _loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n  int _extendRatio = 2; // Expansion multiplier\n  late List<Pair?> _buckets; // Bucket array\n  Pair _TOMBSTONE = Pair(-1, \"-1\"); // Removal marker\n\n  /* Constructor */\n  HashMapOpenAddressing() {\n    _size = 0;\n    _buckets = List.generate(_capacity, (index) => null);\n  }\n\n  /* Hash function */\n  int hashFunc(int key) {\n    return key % _capacity;\n  }\n\n  /* Load factor */\n  double loadFactor() {\n    return _size / _capacity;\n  }\n\n  /* Search for bucket index corresponding to key */\n  int findBucket(int key) {\n    int index = hashFunc(key);\n    int firstTombstone = -1;\n    // Linear probing, break when encountering an empty bucket\n    while (_buckets[index] != null) {\n      // If key is encountered, return the corresponding bucket index\n      if (_buckets[index]!.key == key) {\n        // If a removal marker was encountered before, move the key-value pair to that index\n        if (firstTombstone != -1) {\n          _buckets[firstTombstone] = _buckets[index];\n          _buckets[index] = _TOMBSTONE;\n          return firstTombstone; // Return the moved bucket index\n        }\n        return index; // Return bucket index\n      }\n      // Record the first removal marker encountered\n      if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) {\n        firstTombstone = index;\n      }\n      // Calculate bucket index, wrap around to the head if past the tail\n      index = (index + 1) % _capacity;\n    }\n    // If key does not exist, return the index for insertion\n    return firstTombstone == -1 ? index : firstTombstone;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, return corresponding val\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      return _buckets[index]!.val;\n    }\n    // If key-value pair does not exist, return null\n    return null;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor() > _loadThres) {\n      extend();\n    }\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, overwrite val and return\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index]!.val = val;\n      return;\n    }\n    // If key-value pair does not exist, add the key-value pair\n    _buckets[index] = new Pair(key, val);\n    _size++;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, overwrite it with removal marker\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index] = _TOMBSTONE;\n      _size--;\n    }\n  }\n\n  /* Expand hash table */\n  void extend() {\n    // Temporarily store the original hash table\n    List<Pair?> bucketsTmp = _buckets;\n    // Initialize expanded new hash table\n    _capacity *= _extendRatio;\n    _buckets = List.generate(_capacity, (index) => null);\n    _size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (Pair? pair in bucketsTmp) {\n      if (pair != null && pair != _TOMBSTONE) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Print hash table */\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
    /* Hash table with open addressing */\nstruct HashMapOpenAddressing {\n    size: usize,                // Number of key-value pairs\n    capacity: usize,            // Hash table capacity\n    load_thres: f64,            // Load factor threshold for triggering expansion\n    extend_ratio: usize,        // Expansion multiplier\n    buckets: Vec<Option<Pair>>, // Bucket array\n    TOMBSTONE: Option<Pair>,    // Removal marker\n}\n\nimpl HashMapOpenAddressing {\n    /* Constructor */\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    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        (key % self.capacity as i32) as usize\n    }\n\n    /* Load factor */\n    fn load_factor(&self) -> f64 {\n        self.size as f64 / self.capacity as f64\n    }\n\n    /* Search for bucket index corresponding to 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        // Linear probing, break when encountering an empty bucket\n        while self.buckets[index].is_some() {\n            // If key is found, return corresponding bucket index\n            if self.buckets[index].as_ref().unwrap().key == key {\n                // If deletion marker was encountered before, move key-value pair to that index\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; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE {\n                first_tombstone = index as i32;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % self.capacity;\n        }\n        // If key does not exist, return the index for insertion\n        if first_tombstone == -1 {\n            index\n        } else {\n            first_tombstone as usize\n        }\n    }\n\n    /* Query operation */\n    fn get(&mut self, key: i32) -> Option<&str> {\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, return corresponding 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        // If key-value pair does not exist, return null\n        None\n    }\n\n    /* Add operation */\n    fn put(&mut self, key: i32, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, overwrite val and return\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        // If key-value pair does not exist, add the key-value pair\n        self.buckets[index] = Some(Pair { key, val });\n        self.size += 1;\n    }\n\n    /* Remove operation */\n    fn remove(&mut self, key: i32) {\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, overwrite it with removal marker\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    /* Expand hash table */\n    fn extend(&mut self) {\n        // Temporarily store the original hash table\n        let buckets_tmp = self.buckets.clone();\n        // Initialize expanded new hash table\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![None; self.capacity];\n        self.size = 0;\n\n        // Move key-value pairs from original hash table to new hash table\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    /* Print hash table */\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
    /* Hash table with open addressing */\ntypedef struct {\n    int size;         // Number of key-value pairs\n    int capacity;     // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio;  // Expansion multiplier\n    Pair **buckets;   // Bucket array\n    Pair *TOMBSTONE;  // Removal marker\n} HashMapOpenAddressing;\n\n/* Constructor */\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/* Destructor */\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/* Hash function */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Load factor */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Search for bucket index corresponding to key */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    int firstTombstone = -1;\n    // Linear probing, break when encountering an empty bucket\n    while (hashMap->buckets[index] != NULL) {\n        // If key is encountered, return the corresponding bucket index\n        if (hashMap->buckets[index]->key == key) {\n            // If a removal marker was encountered before, move the key-value pair to that index\n            if (firstTombstone != -1) {\n                hashMap->buckets[firstTombstone] = hashMap->buckets[index];\n                hashMap->buckets[index] = hashMap->TOMBSTONE;\n                return firstTombstone; // Return the moved bucket index\n            }\n            return index; // Return bucket index\n        }\n        // Record the first removal marker encountered\n        if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {\n            firstTombstone = index;\n        }\n        // Calculate bucket index, wrap around to the head if past the tail\n        index = (index + 1) % hashMap->capacity;\n    }\n    // If key does not exist, return the index for insertion\n    return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* Query operation */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, return corresponding val\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        return hashMap->buckets[index]->val;\n    }\n    // Return empty string if key-value pair does not exist\n    return \"\";\n}\n\n/* Add operation */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, overwrite val and return\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    // If key-value pair does not exist, add the key-value pair\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/* Remove operation */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, overwrite it with removal marker\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/* Expand hash table */\nvoid extend(HashMapOpenAddressing *hashMap) {\n    // Temporarily store the original hash table\n    Pair **bucketsTmp = hashMap->buckets;\n    int oldCapacity = hashMap->capacity;\n    // Initialize expanded new hash table\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->size = 0;\n    // Move key-value pairs from original hash table to new hash table\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/* Print hash table */\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
    /* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private var size: Int               // Number of key-value pairs\n    private var capacity: Int           // Hash table capacity\n    private val loadThres: Double       // Load factor threshold for triggering expansion\n    private val extendRatio: Int        // Expansion multiplier\n    private var buckets: Array<Pair?>   // Bucket array\n    private val TOMBSTONE: Pair         // Removal marker\n\n    /* Constructor */\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    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Load factor */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Search for bucket index corresponding to key */\n    fun findBucket(key: Int): Int {\n        var index = hashFunc(key)\n        var firstTombstone = -1\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index]?.key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Return the moved bucket index\n                }\n                return index // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity\n        }\n        // If key does not exist, return the index for insertion\n        return if (firstTombstone == -1) index else firstTombstone\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index]?._val\n        }\n        // If key-value pair does not exist, return null\n        return null\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index]!!._val = _val\n            return\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = Pair(key, _val)\n        size++\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE\n            size--\n        }\n    }\n\n    /* Expand hash table */\n    fun extend() {\n        // Temporarily store the original hash table\n        val bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = arrayOfNulls(capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for (pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Print hash table */\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
    ### Hash map with open addressing ###\nclass HashMapOpenAddressing\n  TOMBSTONE = Pair.new(-1, '-1') # Removal marker\n\n  ### Constructor ###\n  def initialize\n    @size = 0 # Number of key-value pairs\n    @capacity = 4 # Hash table capacity\n    @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion\n    @extend_ratio = 2 # Expansion multiplier\n    @buckets = Array.new(@capacity) # Bucket array\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Load factor ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Search bucket index for key ###\n  def find_bucket(key)\n    index = hash_func(key)\n    first_tombstone = -1\n    # Linear probing, break when encountering an empty bucket\n    while !@buckets[index].nil?\n      # If key is encountered, return the corresponding bucket index\n      if @buckets[index].key == key\n        # If a removal marker was encountered before, move the key-value pair to that index\n        if first_tombstone != -1\n          @buckets[first_tombstone] = @buckets[index]\n          @buckets[index] = TOMBSTONE\n          return first_tombstone # Return the moved bucket index\n        end\n        return index # Return bucket index\n      end\n      # Record the first removal marker encountered\n      first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE\n      # Calculate bucket index, wrap around to the head if past the tail\n      index = (index + 1) % @capacity\n    end\n    # If key does not exist, return the index for insertion\n    first_tombstone == -1 ? index : first_tombstone\n  end\n\n  ### Query operation ###\n  def get(key)\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair is found, return corresponding val\n    return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n    # Return nil if key-value pair does not exist\n    nil\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    # When load factor exceeds threshold, perform expansion\n    extend if load_factor > @load_thres\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair found, overwrite val and return\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index].val = val\n      return\n    end\n    # If key-value pair does not exist, add the key-value pair\n    @buckets[index] = Pair.new(key, val)\n    @size += 1\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair is found, overwrite it with removal marker\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index] = TOMBSTONE\n      @size -= 1\n    end\n  end\n\n  ### Expand hash table ###\n  def extend\n    # Temporarily store the original hash table\n    buckets_tmp = @buckets\n    # Initialize expanded new hash table\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity)\n    @size = 0\n    # Move key-value pairs from original hash table to new hash table\n    for pair in buckets_tmp\n      put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n    end\n  end\n\n  ### Print hash table ###\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":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2-quadratic-probing","level":3,"title":"2.   Quadratic Probing","text":"

    Quadratic probing is similar to linear probing and is one of the common strategies for open addressing. When a collision occurs, quadratic probing does not simply skip a fixed number of steps but skips a number of steps equal to the \"square of the number of probes\", i.e., \\(1, 4, 9, \\dots\\) steps.

    Quadratic probing has the following advantages:

    • Quadratic probing attempts to alleviate the clustering effect of linear probing by skipping distances equal to the square of the probe count.
    • Quadratic probing skips larger distances to find empty positions, which helps to distribute data more evenly.

    However, quadratic probing is not perfect:

    • Clustering still exists, i.e., some positions are more likely to be occupied than others.
    • Due to the growth of squares, quadratic probing may not probe the entire hash table, meaning that even if there are empty buckets in the hash table, quadratic probing may not be able to access them.
    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3-multiple-hashing","level":3,"title":"3.   Multiple Hashing","text":"

    As the name suggests, multiple hashing uses multiple hash functions \\(f_1(x)\\), \\(f_2(x)\\), \\(f_3(x)\\), \\(\\dots\\) for probing.

    • Inserting elements: If hash function \\(f_1(x)\\) encounters a conflict, try \\(f_2(x)\\), and so on, until an empty position is found and the element is inserted.
    • Searching for elements: Search in the same order of hash functions until the target element is found and return it; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return None.

    Compared with linear probing, multiple hashing is less prone to clustering, but using multiple hash functions introduces additional computational overhead.

    Tip

    Please note that hash tables based on open addressing, including linear probing, quadratic probing, and multiple hashing, all have the problem that elements cannot be deleted directly.

    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623-choice-of-programming-languages","level":2,"title":"6.2.3   Choice of Programming Languages","text":"

    Different programming languages adopt different hash table implementation strategies. Here are a few examples:

    • Python uses open addressing. The dict dictionary uses pseudo-random numbers for probing.
    • Java uses separate chaining. Since JDK 1.8, when the array length in HashMap reaches 64 and the length of a linked list reaches 8, the linked list is converted to a red-black tree to improve search performance.
    • Go uses separate chaining. Go stipulates that each bucket can store up to 8 key-value pairs, and if the capacity is exceeded, an overflow bucket is linked; when there are too many overflow buckets, a special equal-capacity expansion operation is performed to ensure performance.
    ","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   Hash Table","text":"

    A hash table, also known as a hash map, stores mappings from keys key to values value, enabling efficient lookups. Specifically, given a key key, we can retrieve the corresponding value value from a hash table in \\(O(1)\\) time.

    As shown below, suppose we have \\(n\\) students, each with two pieces of information: a name and a student ID. If we want to support the query \"given a student ID, return the corresponding name,\" we can use the hash table shown below.

    Figure 6-1   Abstract representation of a hash table

    In addition to hash tables, arrays and linked lists can also implement query functionality. Their efficiency comparison is shown in the following table.

    • Adding elements: Simply add elements to the end of the array (linked list), using \\(O(1)\\) time.
    • Querying elements: Since the array (linked list) is unordered, all elements need to be traversed, using \\(O(n)\\) time.
    • Deleting elements: The element must first be located, then deleted from the array (linked list), using \\(O(n)\\) time.

    Table 6-1   Comparison of element query efficiency

    Array Linked List Hash Table Find element \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) Add element \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Delete element \\(O(n)\\) \\(O(n)\\) \\(O(1)\\)

    As we can see, insertion, deletion, lookup, and update operations in a hash table all have time complexity \\(O(1)\\), making hash tables highly efficient.

    ","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#611-common-hash-table-operations","level":2,"title":"6.1.1   Common Hash Table Operations","text":"

    Common operations on hash tables include: initialization, query operations, adding key-value pairs, and deleting key-value pairs. Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
    # Initialize hash table\nhmap: dict = {}\n\n# Add operation\n# Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n# Query operation\n# Input key into hash table to get value\nname: str = hmap[15937]\n\n# Delete operation\n# Delete key-value pair (key, value) from hash table\nhmap.pop(10583)\n
    hash_map.cpp
    /* Initialize hash table */\nunordered_map<int, string> map;\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\";\nmap[15937] = \"XiaoLuo\";\nmap[16750] = \"XiaoSuan\";\nmap[13276] = \"XiaoFa\";\nmap[10583] = \"XiaoYa\";\n\n/* Query operation */\n// Input key into hash table to get value\nstring name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.erase(10583);\n
    hash_map.java
    /* Initialize hash table */\nMap<Integer, String> map = new HashMap<>();\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.put(12836, \"XiaoHa\");\nmap.put(15937, \"XiaoLuo\");\nmap.put(16750, \"XiaoSuan\");\nmap.put(13276, \"XiaoFa\");\nmap.put(10583, \"XiaoYa\");\n\n/* Query operation */\n// Input key into hash table to get value\nString name = map.get(15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583);\n
    hash_map.cs
    /* Initialize hash table */\nDictionary<int, string> map = new() {\n    /* Add operation */\n    // Add key-value pair (key, value) to hash table\n    { 12836, \"XiaoHa\" },\n    { 15937, \"XiaoLuo\" },\n    { 16750, \"XiaoSuan\" },\n    { 13276, \"XiaoFa\" },\n    { 10583, \"XiaoYa\" }\n};\n\n/* Query operation */\n// Input key into hash table to get value\nstring name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.Remove(10583);\n
    hash_map_test.go
    /* Initialize hash table */\nhmap := make(map[int]string)\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nname := hmap[15937]\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\ndelete(hmap, 10583)\n
    hash_map.swift
    /* Initialize hash table */\nvar map: [Int: String] = [:]\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\"\nmap[15937] = \"XiaoLuo\"\nmap[16750] = \"XiaoSuan\"\nmap[13276] = \"XiaoFa\"\nmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map[15937]!\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.removeValue(forKey: 10583)\n
    hash_map.js
    /* Initialize hash table */\nconst map = new Map();\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.set(12836, 'XiaoHa');\nmap.set(15937, 'XiaoLuo');\nmap.set(16750, 'XiaoSuan');\nmap.set(13276, 'XiaoFa');\nmap.set(10583, 'XiaoYa');\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map.get(15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.delete(10583);\n
    hash_map.ts
    /* Initialize hash table */\nconst map = new Map<number, string>();\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.set(12836, 'XiaoHa');\nmap.set(15937, 'XiaoLuo');\nmap.set(16750, 'XiaoSuan');\nmap.set(13276, 'XiaoFa');\nmap.set(10583, 'XiaoYa');\nconsole.info('\\nAfter adding, hash table is\\nKey -> Value');\nconsole.info(map);\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map.get(15937);\nconsole.info('\\nInput student ID 15937, queried name ' + name);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.delete(10583);\nconsole.info('\\nAfter deleting 10583, hash table is\\nKey -> Value');\nconsole.info(map);\n
    hash_map.dart
    /* Initialize hash table */\nMap<int, String> map = {};\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\";\nmap[15937] = \"XiaoLuo\";\nmap[16750] = \"XiaoSuan\";\nmap[13276] = \"XiaoFa\";\nmap[10583] = \"XiaoYa\";\n\n/* Query operation */\n// Input key into hash table to get value\nString name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583);\n
    hash_map.rs
    use std::collections::HashMap;\n\n/* Initialize hash table */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.insert(12836, \"XiaoHa\".to_string());\nmap.insert(15937, \"XiaoLuo\".to_string());\nmap.insert(16750, \"XiaoSuan\".to_string());\nmap.insert(13276, \"XiaoFa\".to_string());\nmap.insert(10583, \"XiaoYa\".to_string());\n\n/* Query operation */\n// Input key into hash table to get value\nlet _name: Option<&String> = map.get(&15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nlet _removed_value: Option<String> = map.remove(&10583);\n
    hash_map.c
    // C does not provide a built-in hash table\n
    hash_map.kt
    /* Initialize hash table */\nval map = HashMap<Int,String>()\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\"\nmap[15937] = \"XiaoLuo\"\nmap[16750] = \"XiaoSuan\"\nmap[13276] = \"XiaoFa\"\nmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nval name = map[15937]\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583)\n
    hash_map.rb
    # Initialize hash table\nhmap = {}\n\n# Add operation\n# Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n# Query operation\n# Input key into hash table to get value\nname = hmap[15937]\n\n# Delete operation\n# Delete key-value pair (key, value) from hash table\nhmap.delete(10583)\n
    Visualized Execution

    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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%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

    There are three common ways to traverse a hash table: traversing key-value pairs, traversing keys, and traversing values. Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
    # Traverse hash table\n# Traverse key-value pairs key->value\nfor key, value in hmap.items():\n    print(key, \"->\", value)\n# Traverse keys only\nfor key in hmap.keys():\n    print(key)\n# Traverse values only\nfor value in hmap.values():\n    print(value)\n
    hash_map.cpp
    /* Traverse hash table */\n// Traverse key-value pairs key->value\nfor (auto kv: map) {\n    cout << kv.first << \" -> \" << kv.second << endl;\n}\n// Traverse using iterator key->value\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n    cout << iter->first << \"->\" << iter->second << endl;\n}\n
    hash_map.java
    /* Traverse hash table */\n// Traverse key-value pairs key->value\nfor (Map.Entry<Integer, String> kv: map.entrySet()) {\n    System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// Traverse keys only\nfor (int key: map.keySet()) {\n    System.out.println(key);\n}\n// Traverse values only\nfor (String val: map.values()) {\n    System.out.println(val);\n}\n
    hash_map.cs
    /* Traverse hash table */\n// Traverse key-value pairs Key->Value\nforeach (var kv in map) {\n    Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// Traverse keys only\nforeach (int key in map.Keys) {\n    Console.WriteLine(key);\n}\n// Traverse values only\nforeach (string val in map.Values) {\n    Console.WriteLine(val);\n}\n
    hash_map_test.go
    /* Traverse hash table */\n// Traverse key-value pairs key->value\nfor key, value := range hmap {\n    fmt.Println(key, \"->\", value)\n}\n// Traverse keys only\nfor key := range hmap {\n    fmt.Println(key)\n}\n// Traverse values only\nfor _, value := range hmap {\n    fmt.Println(value)\n}\n
    hash_map.swift
    /* Traverse hash table */\n// Traverse key-value pairs Key->Value\nfor (key, value) in map {\n    print(\"\\(key) -> \\(value)\")\n}\n// Traverse keys only\nfor key in map.keys {\n    print(key)\n}\n// Traverse values only\nfor value in map.values {\n    print(value)\n}\n
    hash_map.js
    /* Traverse hash table */\nconsole.info('\\nTraverse key-value pairs Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nTraverse keys only Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nTraverse values only Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
    hash_map.ts
    /* Traverse hash table */\nconsole.info('\\nTraverse key-value pairs Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nTraverse keys only Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nTraverse values only Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
    hash_map.dart
    /* Traverse hash table */\n// Traverse key-value pairs Key->Value\nmap.forEach((key, value) {\n  print('$key -> $value');\n});\n\n// Traverse keys only\nmap.keys.forEach((key) {\n  print(key);\n});\n\n// Traverse values only\nmap.values.forEach((value) {\n  print(value);\n});\n
    hash_map.rs
    /* Traverse hash table */\n// Traverse key-value pairs Key->Value\nfor (key, value) in &map {\n    println!(\"{key} -> {value}\");\n}\n\n// Traverse keys only\nfor key in map.keys() {\n    println!(\"{key}\");\n}\n\n// Traverse values only\nfor value in map.values() {\n    println!(\"{value}\");\n}\n
    hash_map.c
    // C does not provide a built-in hash table\n
    hash_map.kt
    /* Traverse hash table */\n// Traverse key-value pairs key->value\nfor ((key, value) in map) {\n    println(\"$key -> $value\")\n}\n// Traverse keys only\nfor (key in map.keys) {\n    println(key)\n}\n// Traverse values only\nfor (_val in map.values) {\n    println(_val)\n}\n
    hash_map.rb
    # Traverse hash table\n# Traverse key-value pairs key->value\nhmap.entries.each { |key, value| puts \"#{key} -> #{value}\" }\n\n# Traverse keys only\nhmap.keys.each { |key| puts key }\n\n# Traverse values only\nhmap.values.each { |val| puts val }\n
    Visualized Execution

    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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%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%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%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":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#612-simple-hash-table-implementation","level":2,"title":"6.1.2   Simple Hash Table Implementation","text":"

    Let's start with the simplest case: implementing a hash table with just an array. In a hash table, each empty slot in the array is called a bucket, and each bucket can store one key-value pair. A lookup therefore consists of finding the bucket for key and reading the value stored there.

    So how do we find the right bucket for a given key? We do this with a hash function. A hash function maps a larger input space to a smaller output space. In a hash table, the input space is the set of all keys, and the output space is the set of all buckets (array indices). In other words, given a key, the hash function tells us where the corresponding key-value pair should be stored in the array.

    Given a key, computing the bucket index involves the following two steps:

    1. Use a hash algorithm hash() to compute a hash value.
    2. Take that hash value modulo the number of buckets (array length), capacity, to obtain the bucket (array index) index corresponding to the key.
    index = hash(key) % capacity\n

    We can then use index to access the corresponding bucket in the hash table and retrieve the value.

    Suppose the array length is capacity = 100 and the hash algorithm is hash(key) = key. Then the hash function is key % 100. Figure 6-2 illustrates how this hash function works, using student ID as key and name as value.

    Figure 6-2   Working principle of hash function

    The following code implements a simple hash table. Here, we encapsulate key and value into a class Pair to represent a key-value pair.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py
    class Pair:\n    \"\"\"Key-value pair\"\"\"\n\n    def __init__(self, key: int, val: str):\n        self.key = key\n        self.val = val\n\nclass ArrayHashMap:\n    \"\"\"Hash table based on array implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        # Initialize array with 100 buckets\n        self.buckets: list[Pair | None] = [None] * 100\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        index = key % 100\n        return index\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Query operation\"\"\"\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        \"\"\"Add and update operation\"\"\"\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        \"\"\"Remove operation\"\"\"\n        index: int = self.hash_func(key)\n        # Set to None to represent removal\n        self.buckets[index] = None\n\n    def entry_set(self) -> list[Pair]:\n        \"\"\"Get all key-value pairs\"\"\"\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        \"\"\"Get all keys\"\"\"\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        \"\"\"Get all values\"\"\"\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        \"\"\"Print hash table\"\"\"\n        for pair in self.buckets:\n            if pair is not None:\n                print(pair.key, \"->\", pair.val)\n
    array_hash_map.cpp
    /* Key-value pair */\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/* Hash table based on array implementation */\nclass ArrayHashMap {\n  private:\n    vector<Pair *> buckets;\n\n  public:\n    ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = vector<Pair *>(100);\n    }\n\n    ~ArrayHashMap() {\n        // Free memory\n        for (const auto &bucket : buckets) {\n            delete bucket;\n        }\n        buckets.clear();\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        // Free memory and set to nullptr\n        delete buckets[index];\n        buckets[index] = nullptr;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\n    void print() {\n        for (Pair *kv : pairSet()) {\n            cout << kv->key << \" -> \" << kv->val << endl;\n        }\n    }\n};\n
    array_hash_map.java
    /* Key-value pair */\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/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private List<Pair> buckets;\n\n    public ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = new ArrayList<>();\n        for (int i = 0; i < 100; i++) {\n            buckets.add(null);\n        }\n    }\n\n    /* Hash function */\n    private int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    public void remove(int key) {\n        int index = hashFunc(key);\n        // Set to null to represent deletion\n        buckets.set(index, null);\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair int->string */\nclass Pair(int key, string val) {\n    public int key = key;\n    public string val = val;\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    List<Pair?> buckets;\n    public ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = [];\n        for (int i = 0; i < 100; i++) {\n            buckets.Add(null);\n        }\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Set to null to represent deletion\n        buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair */\ntype pair struct {\n    key int\n    val string\n}\n\n/* Hash table based on array implementation */\ntype arrayHashMap struct {\n    buckets []*pair\n}\n\n/* Initialize hash table */\nfunc newArrayHashMap() *arrayHashMap {\n    // Initialize array with 100 buckets\n    buckets := make([]*pair, 100)\n    return &arrayHashMap{buckets: buckets}\n}\n\n/* Hash function */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n    index := key % 100\n    return index\n}\n\n/* Query operation */\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/* Add operation */\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/* Remove operation */\nfunc (a *arrayHashMap) remove(key int) {\n    index := a.hashFunc(key)\n    // Set to nil to delete\n    a.buckets[index] = nil\n}\n\n/* Get all key pairs */\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/* Get all keys */\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/* Get all values */\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/* Print hash table */\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
    /* Key-value pair */\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/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private var buckets: [Pair?]\n\n    init() {\n        // Initialize array with 100 buckets\n        buckets = Array(repeating: nil, count: 100)\n    }\n\n    /* Hash function */\n    private func hashFunc(key: Int) -> Int {\n        let index = key % 100\n        return index\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let pair = buckets[index]\n        return pair?.val\n    }\n\n    /* Add operation */\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    /* Remove operation */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        // Set to nil to delete\n        buckets[index] = nil\n    }\n\n    /* Get all key-value pairs */\n    func pairSet() -> [Pair] {\n        buckets.compactMap { $0 }\n    }\n\n    /* Get all keys */\n    func keySet() -> [Int] {\n        buckets.compactMap { $0?.key }\n    }\n\n    /* Get all values */\n    func valueSet() -> [String] {\n        buckets.compactMap { $0?.val }\n    }\n\n    /* Print hash table */\n    func print() {\n        for pair in pairSet() {\n            Swift.print(\"\\(pair.key) -> \\(pair.val)\")\n        }\n    }\n}\n
    array_hash_map.js
    /* Key-value pair Number -> String */\nclass Pair {\n    constructor(key, val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    #buckets;\n    constructor() {\n        // Initialize array with 100 buckets\n        this.#buckets = new Array(100).fill(null);\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % 100;\n    }\n\n    /* Query operation */\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    /* Add operation */\n    set(key, val) {\n        let index = this.#hashFunc(key);\n        this.#buckets[index] = new Pair(key, val);\n    }\n\n    /* Remove operation */\n    delete(key) {\n        let index = this.#hashFunc(key);\n        // Set to null to represent deletion\n        this.#buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair 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/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private readonly buckets: (Pair | null)[];\n\n    constructor() {\n        // Initialize array with 100 buckets\n        this.buckets = new Array(100).fill(null);\n    }\n\n    /* Hash function */\n    private hashFunc(key: number): number {\n        return key % 100;\n    }\n\n    /* Query operation */\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    /* Add operation */\n    public set(key: number, val: string) {\n        let index = this.hashFunc(key);\n        this.buckets[index] = new Pair(key, val);\n    }\n\n    /* Remove operation */\n    public delete(key: number) {\n        let index = this.hashFunc(key);\n        // Set to null to represent deletion\n        this.buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair */\nclass Pair {\n  int key;\n  String val;\n  Pair(this.key, this.val);\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n  late List<Pair?> _buckets;\n\n  ArrayHashMap() {\n    // Initialize array with 100 buckets\n    _buckets = List.filled(100, null);\n  }\n\n  /* Hash function */\n  int _hashFunc(int key) {\n    final int index = key % 100;\n    return index;\n  }\n\n  /* Query operation */\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  /* Add operation */\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  /* Remove operation */\n  void remove(int key) {\n    final int index = _hashFunc(key);\n    _buckets[index] = null;\n  }\n\n  /* Get all key-value pairs */\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  /* Get all keys */\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  /* Get all values */\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  /* Print hash table */\n  void printHashMap() {\n    for (final Pair kv in pairSet()) {\n      print(\"${kv.key} -> ${kv.val}\");\n    }\n  }\n}\n
    array_hash_map.rs
    /* Key-value pair */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n    pub key: i32,\n    pub val: String,\n}\n\n/* Hash table based on array implementation */\npub struct ArrayHashMap {\n    buckets: Vec<Option<Pair>>,\n}\n\nimpl ArrayHashMap {\n    pub fn new() -> ArrayHashMap {\n        // Initialize array with 100 buckets\n        Self {\n            buckets: vec![None; 100],\n        }\n    }\n\n    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % 100\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    pub fn remove(&mut self, key: i32) {\n        let index = self.hash_func(key);\n        // Set to None to represent removal\n        self.buckets[index] = None;\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    /* Key-value pair int->string */\ntypedef struct {\n    int key;\n    char *val;\n} Pair;\n\n/* Hash table based on array implementation */\ntypedef struct {\n    Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* Constructor */\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/* Destructor */\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/* Add operation */\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/* Remove operation */\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/* Get all key-value pairs */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n    Pair *entries;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\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/* Get all keys */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n    int *keys;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\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/* Get all values */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n    char **vals;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\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/* Print hash table */\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
    /* Key-value pair */\nclass Pair(\n    var key: Int,\n    var _val: String\n)\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    // Initialize array with 100 buckets\n    private val buckets = arrayOfNulls<Pair>(100)\n\n    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        val index = key % 100\n        return index\n    }\n\n    /* Query operation */\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    /* Add operation */\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    /* Remove operation */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        // Set to null to represent deletion\n        buckets[index] = null\n    }\n\n    /* Get all key-value pairs */\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    /* Get all keys */\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    /* Get all values */\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    /* Print hash table */\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
    ### Key-value pair ###\nclass Pair\n  attr_accessor :key, :val\n\n  def initialize(key, val)\n    @key = key\n    @val = val\n  end\nend\n\n### Hash map based on array ###\nclass ArrayHashMap\n  ### Constructor ###\n  def initialize\n    # Initialize array with 100 buckets\n    @buckets = Array.new(100)\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    index = key % 100\n  end\n\n  ### Query operation ###\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  ### Add operation ###\n  def put(key, val)\n    pair = Pair.new(key, val)\n    index = hash_func(key)\n    @buckets[index] = pair\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    index = hash_func(key)\n    # Set to nil to delete\n    @buckets[index] = nil\n  end\n\n  ### Get all key-value pairs ###\n  def entry_set\n    result = []\n    @buckets.each { |pair| result << pair unless pair.nil? }\n    result\n  end\n\n  ### Get all keys ###\n  def key_set\n    result = []\n    @buckets.each { |pair| result << pair.key unless pair.nil? }\n    result\n  end\n\n  ### Get all values ###\n  def value_set\n    result = []\n    @buckets.each { |pair| result << pair.val unless pair.nil? }\n    result\n  end\n\n  ### Print hash table ###\n  def print\n    @buckets.each { |pair| puts \"#{pair.key} -> #{pair.val}\" unless pair.nil? }\n  end\nend\n
    ","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#613-hash-collision-and-resizing","level":2,"title":"6.1.3   Hash Collision and Resizing","text":"

    Fundamentally, a hash function maps the input space consisting of all keys to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, in theory, different inputs must sometimes map to the same output.

    For the hash function in the above example, when the input keys have the same last two digits, the hash function produces the same output. For example, when querying two students with IDs 12836 and 20336, we get:

    12836 % 100 = 36\n20336 % 100 = 36\n

    As shown below, two student IDs now point to the same name, which is clearly incorrect. We call this situation, where multiple inputs map to the same output, a hash collision.

    Figure 6-3   Hash collision example

    It's easy to see that the larger the hash table capacity \\(n\\), the lower the probability that multiple keys will be assigned to the same bucket, and the fewer collisions. Therefore, we can reduce hash collisions by expanding the hash table.

    As shown in Figure 6-4, before expansion, the key-value pairs (136, A) and (236, D) collided, but after expansion, the collision disappears.

    Figure 6-4   Hash table resizing

    Like resizing an array, resizing a hash table requires migrating all key-value pairs from the original table to the new table, which is expensive. In addition, because the hash table capacity capacity changes, we must recompute the storage location of every key-value pair using the hash function, which further increases the cost of resizing. For this reason, programming languages typically reserve a sufficiently large hash table capacity to avoid frequent resizing.

    The load factor is an important concept in hash tables. It is defined as the number of elements in the hash table divided by the number of buckets and is used to measure the severity of hash collisions. It is also commonly used as a threshold for triggering hash table resizing. For example, in Java, when the load factor exceeds \\(0.75\\), the system expands the hash table to twice its original size.

    ","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   Summary","text":"","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_hashing/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Given an input key, a hash table can retrieve the corresponding value in \\(O(1)\\) time, which is highly efficient.
    • Common hash table operations include querying, adding key-value pairs, deleting key-value pairs, and traversing the hash table.
    • The hash function maps a key to an array index, allowing access to the corresponding bucket and retrieval of the value.
    • Two different keys may end up with the same array index after hashing, leading to erroneous query results. This phenomenon is known as hash collision.
    • The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table expansion can mitigate hash collisions. Similar to array expansion, hash table expansion is costly.
    • The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table expansion.
    • Separate chaining addresses hash collisions by storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by further converting the linked lists into red-black trees.
    • Open addressing handles hash collisions through multiple probing. Linear probing uses a fixed step size but cannot delete elements and is prone to clustering. Double hashing uses multiple hash functions for probing, which reduces clustering compared to linear probing but increases computational overhead.
    • Different programming languages adopt various hash table implementations. For example, Java's HashMap uses separate chaining, while Python's dict employs open addressing.
    • In hash tables, we desire hash algorithms with determinism, high efficiency, and uniform distribution. In cryptography, hash algorithms should also possess collision resistance and the avalanche effect.
    • Hash algorithms typically use large prime numbers as moduli to maximize the uniform distribution of hash values and reduce hash collisions.
    • Common hash algorithms include MD5, SHA-1, SHA-2, and SHA-3. MD5 is often used for file integrity checks, while SHA-2 is commonly used in secure applications and protocols.
    • Programming languages usually provide built-in hash algorithms for data types to calculate bucket indices in hash tables. Generally, only immutable objects are hashable.
    ","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: When does the time complexity of a hash table degrade to \\(O(n)\\)?

    The time complexity of a hash table can degrade to \\(O(n)\\) when hash collisions are severe. When the hash function is well-designed, the capacity is set appropriately, and collisions are evenly distributed, the time complexity is \\(O(1)\\). We usually consider the time complexity to be \\(O(1)\\) when using built-in hash tables in programming languages.

    Q: Why not use the hash function \\(f(x) = x\\)? This would eliminate collisions.

    Under the hash function \\(f(x) = x\\), each element corresponds to a unique bucket index, which is equivalent to an array. However, the input space is usually much larger than the output space (array length), so the last step of a hash function is often to take the modulo of the array length. In other words, the goal of a hash table is to map a larger state space to a smaller one while providing \\(O(1)\\) query efficiency.

    Q: Why can hash tables be more efficient than arrays, linked lists, or binary trees, even though hash tables are implemented using these structures?

    Firstly, hash tables have higher time efficiency but lower space efficiency. A significant portion of memory in hash tables remains unused.

    Secondly, hash tables are only more time-efficient in specific use cases. If a feature can be implemented with the same time complexity using an array or a linked list, it's usually faster than using a hash table. This is because the computation of the hash function incurs overhead, making the constant factor in the time complexity larger.

    Lastly, the time complexity of hash tables can degrade. For example, in separate chaining, we perform search operations in a linked list or red-black tree, which still risks degrading to \\(O(n)\\) time.

    Q: Does double hashing also have the flaw of not being able to delete elements directly? Can space marked as deleted be reused?

    Double hashing is a form of open addressing, and all open addressing methods have the drawback of not being able to delete elements directly; they require marking elements as deleted. Marked spaces can be reused. When inserting new elements into the hash table, and the hash function points to a position marked as deleted, that position can be used by the new element. This maintains the probing sequence of the hash table while ensuring efficient use of space.

    Q: Why do hash collisions occur during the search process in linear probing?

    During the search process, the hash function points to the corresponding bucket and key-value pair. If the key doesn't match, it indicates a hash collision. Therefore, linear probing will search downward at a predetermined step size until the correct key-value pair is found or the search fails.

    Q: Why can expanding a hash table alleviate hash collisions?

    The last step of a hash function often involves taking the modulo of the array length \\(n\\), to keep the output within the array index range. When expanding, the array length \\(n\\) changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after expansion, thereby mitigating hash collisions.

    Q: If the goal is efficient access, why not just use an array directly?

    When the key values are continuous integers within a small range, an array is indeed a simple and efficient choice. But when the key is of another type, such as a string, we need a hash function to map the key to an array index and then store the element in a bucket array. That structure is precisely what a hash table is.

    ","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"Chapter 8.   Heap","text":"

    Abstract

    Heaps are like mountain peaks, rising layer upon layer, each with a distinct shape.

    The peaks rise and fall at varying heights, yet the tallest peak always catches the eye first.

    ","path":["Chapter 8. Heap","Chapter 8.   Heap"],"tags":[]},{"location":"chapter_heap/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 8.1   Heap
    • 8.2   Heap Construction Operation
    • 8.3   Top-k Problem
    • 8.4   Summary
    ","path":["Chapter 8. Heap","Chapter 8.   Heap"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   Heap Construction Operation","text":"

    In some cases, we want to build a heap using all elements of a list, and this process is called \"heap construction operation.\"

    ","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#821-implementing-with-element-insertion","level":2,"title":"8.2.1   Implementing with Element Insertion","text":"

    We first create an empty heap, then iterate through the list, performing the \"element insertion operation\" on each element in sequence. This means appending the element to the end of the heap and then performing \"bottom-to-top\" heapify on that element.

    Each time an element is inserted into the heap, the heap's length increases by one. Since nodes are added to the binary tree sequentially from top to bottom, the heap is constructed \"from top to bottom.\"

    Given \\(n\\) elements, each element's insertion operation takes \\(O(\\log{n})\\) time, so the time complexity of this heap construction method is \\(O(n \\log n)\\).

    ","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#822-implementing-through-heapify-traversal","level":2,"title":"8.2.2   Implementing Through Heapify Traversal","text":"

    In fact, we can implement a more efficient heap construction method in two steps.

    1. Add all elements of the list as-is to the heap, at which point the heap property is not yet satisfied.
    2. Traverse the heap in reverse order (reverse of level-order traversal), performing \"top-to-bottom heapify\" on each non-leaf node in sequence.

    After heapifying a node, the subtree rooted at that node becomes a valid sub-heap. Since we traverse in reverse order, the heap is constructed \"from bottom to top.\"

    The reason for choosing reverse-order traversal is that it ensures the subtrees beneath the current node are already valid sub-heaps, so heapifying the current node is effective.

    It's worth noting that since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification. As shown in the code below, the last non-leaf node is the parent of the last node; we start from that node and heapify while traversing in reverse order:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def __init__(self, nums: list[int]):\n    \"\"\"Constructor, build heap based on input list\"\"\"\n    # Add list elements to heap as is\n    self.max_heap = nums\n    # Heapify all nodes except leaf nodes\n    for i in range(self.parent(self.size() - 1), -1, -1):\n        self.sift_down(i)\n
    my_heap.cpp
    /* Constructor, build heap based on input list */\nMaxHeap(vector<int> nums) {\n    // Add list elements to heap as is\n    maxHeap = nums;\n    // Heapify all nodes except leaf nodes\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
    my_heap.java
    /* Constructor, build heap based on input list */\nMaxHeap(List<Integer> nums) {\n    // Add list elements to heap as is\n    maxHeap = new ArrayList<>(nums);\n    // Heapify all nodes except leaf nodes\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
    my_heap.cs
    /* Constructor, build heap from input list */\nMaxHeap(IEnumerable<int> nums) {\n    // Add list elements to heap as is\n    maxHeap = new List<int>(nums);\n    // Heapify all nodes except leaf nodes\n    var size = Parent(this.Size() - 1);\n    for (int i = size; i >= 0; i--) {\n        SiftDown(i);\n    }\n}\n
    my_heap.go
    /* Constructor, build heap from slice */\nfunc newMaxHeap(nums []any) *maxHeap {\n    // Add list elements to heap as is\n    h := &maxHeap{data: nums}\n    for i := h.parent(len(h.data) - 1); i >= 0; i-- {\n        // Heapify all nodes except leaf nodes\n        h.siftDown(i)\n    }\n    return h\n}\n
    my_heap.swift
    /* Constructor, build heap based on input list */\ninit(nums: [Int]) {\n    // Add list elements to heap as is\n    maxHeap = nums\n    // Heapify all nodes except leaf nodes\n    for i in (0 ... parent(i: size() - 1)).reversed() {\n        siftDown(i: i)\n    }\n}\n
    my_heap.js
    /* Constructor, build empty heap or build heap from input list */\nconstructor(nums) {\n    // Add list elements to heap as is\n    this.#maxHeap = nums === undefined ? [] : [...nums];\n    // Heapify all nodes except leaf nodes\n    for (let i = this.#parent(this.size() - 1); i >= 0; i--) {\n        this.#siftDown(i);\n    }\n}\n
    my_heap.ts
    /* Constructor, build empty heap or build heap from input list */\nconstructor(nums?: number[]) {\n    // Add list elements to heap as is\n    this.maxHeap = nums === undefined ? [] : [...nums];\n    // Heapify all nodes except leaf nodes\n    for (let i = this.parent(this.size() - 1); i >= 0; i--) {\n        this.siftDown(i);\n    }\n}\n
    my_heap.dart
    /* Constructor, build heap based on input list */\nMaxHeap(List<int> nums) {\n  // Add list elements to heap as is\n  _maxHeap = nums;\n  // Heapify all nodes except leaf nodes\n  for (int i = _parent(size() - 1); i >= 0; i--) {\n    siftDown(i);\n  }\n}\n
    my_heap.rs
    /* Constructor, build heap based on input list */\nfn new(nums: Vec<i32>) -> Self {\n    // Add list elements to heap as is\n    let mut heap = MaxHeap { max_heap: nums };\n    // Heapify all nodes except leaf nodes\n    for i in (0..=Self::parent(heap.size() - 1)).rev() {\n        heap.sift_down(i);\n    }\n    heap\n}\n
    my_heap.c
    /* Constructor, build heap from slice */\nMaxHeap *newMaxHeap(int nums[], int size) {\n    // Push all elements to heap\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 all nodes except leaf nodes\n        siftDown(maxHeap, i);\n    }\n    return maxHeap;\n}\n
    my_heap.kt
    /* Max heap */\nclass MaxHeap(nums: MutableList<Int>?) {\n    // Use list instead of array, no need to consider capacity expansion\n    private val maxHeap = mutableListOf<Int>()\n\n    /* Constructor, build heap based on input list */\n    init {\n        // Add list elements to heap as is\n        maxHeap.addAll(nums!!)\n        // Heapify all nodes except leaf nodes\n        for (i in parent(size() - 1) downTo 0) {\n            siftDown(i)\n        }\n    }\n\n    /* Get index of left child node */\n    private fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Get index of right child node */\n    private fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Get index of parent node */\n    private fun parent(i: Int): Int {\n        return (i - 1) / 2 // Floor division\n    }\n\n    /* Swap elements */\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    /* Get heap size */\n    fun size(): Int {\n        return maxHeap.size\n    }\n\n    /* Check if heap is empty */\n    fun isEmpty(): Boolean {\n        /* Check if heap is empty */\n        return size() == 0\n    }\n\n    /* Access top element */\n    fun peek(): Int {\n        return maxHeap[0]\n    }\n\n    /* Element enters heap */\n    fun push(_val: Int) {\n        // Add node\n        maxHeap.add(_val)\n        // Heapify from bottom to top\n        siftUp(size() - 1)\n    }\n\n    /* Starting from node i, heapify from bottom to top */\n    private fun siftUp(it: Int) {\n        // Kotlin function parameters are immutable, so create temporary variable\n        var i = it\n        while (true) {\n            // Get parent node of node i\n            val p = parent(i)\n            // When \"crossing root node\" or \"node needs no repair\", end heapify\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // Swap two nodes\n            swap(i, p)\n            // Loop upward heapify\n            i = p\n        }\n    }\n\n    /* Element exits heap */\n    fun pop(): Int {\n        // Handle empty case\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Delete node\n        swap(0, size() - 1)\n        // Remove node\n        val _val = maxHeap.removeAt(size() - 1)\n        // Return top element\n        siftDown(0)\n        // Return heap top element\n        return _val\n    }\n\n    /* Starting from node i, heapify from top to bottom */\n    private fun siftDown(it: Int) {\n        // Kotlin function parameters are immutable, so create temporary variable\n        var i = it\n        while (true) {\n            // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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            // Swap two nodes\n            if (ma == i) break\n            // Swap two nodes\n            swap(i, ma)\n            // Loop downwards heapification\n            i = ma\n        }\n    }\n\n    /* Driver Code */\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
    ### Constructor, build heap from input list ###\ndef initialize(nums)\n  # Add list elements to heap as is\n  @max_heap = nums\n  # Heapify all nodes except leaf nodes\n  parent(size - 1).downto(0) do |i|\n    sift_down(i)\n  end\nend\n
    ","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#823-complexity-analysis","level":2,"title":"8.2.3   Complexity Analysis","text":"

    Next, let's attempt to derive the time complexity of this second heap construction method.

    • Assuming the complete binary tree has \\(n\\) nodes, then the number of leaf nodes is \\((n + 1) / 2\\), where \\(/\\) is floor division. Therefore, the number of nodes that need heapification is \\((n - 1) / 2\\).
    • In the top-to-bottom heapify process, each node can sink at most to a leaf node, so the maximum number of iterations is the height of the binary tree, \\(\\log n\\).

    Multiplying these two together, we get a time complexity of \\(O(n \\log n)\\) for the heap construction process. However, this estimate is not accurate because it doesn't account for the property that binary trees have far more nodes at lower levels than at upper levels.

    Let's perform a more accurate calculation. To simplify the analysis, assume a \"perfect binary tree\" with \\(n\\) nodes and height \\(h\\); this assumption does not affect the correctness of the result.

    Figure 8-5   Node count at each level of a perfect binary tree

    As shown in Figure 8-5, the maximum number of iterations for a node's \"top-to-bottom heapify\" equals the distance from that node to a leaf node, which is precisely the node's height. Therefore, we can sum the \"number of nodes \\(\\times\\) node height\" at each level to obtain the total number of heapify iterations for all nodes.

    \\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\]

    Simplifying the expression above requires some high-school sequence algebra. First, multiply \\(T(h)\\) by \\(2\\) to get:

    \\[ \\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} \\]

    Using subtraction of shifted sums, subtract the first equation \\(T(h)\\) from the second equation \\(2 T(h)\\) to get:

    \\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\]

    Observing the above expression, we find that \\(T(h)\\) is a geometric series, which can be calculated directly using the sum formula, yielding a time complexity of:

    \\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\]

    Furthermore, a perfect binary tree with height \\(h\\) has \\(n = 2^{h+1} - 1\\) nodes, so the complexity is \\(O(2^h) = O(n)\\). This derivation shows that the time complexity of building a heap from an input list is \\(O(n)\\), which is highly efficient.

    ","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   Heap","text":"

    A heap is a complete binary tree that satisfies specific conditions and can be mainly categorized into two types, as shown in Figure 8-1.

    • min heap: The value of any node \\(\\leq\\) the values of its child nodes.
    • max heap: The value of any node \\(\\geq\\) the values of its child nodes.

    Figure 8-1   Min heap and max heap

    As a special case of a complete binary tree, heaps have the following characteristics.

    • The bottom layer nodes are filled from left to right, and nodes in other layers are fully filled.
    • We call the root node of the binary tree the \"heap top\" and the bottom-rightmost node the \"heap bottom.\"
    • For max heaps (min heaps), the value of the heap top element (root node) is the largest (smallest).
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#811-common-heap-operations","level":2,"title":"8.1.1   Common Heap Operations","text":"

    It should be noted that many programming languages provide a priority queue, an abstract data structure defined as a queue whose elements are ordered by priority.

    In fact, heaps are typically used to implement priority queues, with max heaps corresponding to priority queues where elements are dequeued in descending order. From a usage perspective, we can regard \"priority queue\" and \"heap\" as equivalent data structures. Therefore, this book does not make a special distinction between the two and uniformly refers to them as \"heap.\"

    Common heap operations are shown in Table 8-1, and method names need to be determined based on the programming language.

    Table 8-1   Efficiency of Heap Operations

    Method name Description Time complexity push() Insert an element into the heap \\(O(\\log n)\\) pop() Remove the heap top element \\(O(\\log n)\\) peek() Access the heap top element (max/min value for max/min heap) \\(O(1)\\) size() Get the number of elements in the heap \\(O(1)\\) isEmpty() Check if the heap is empty \\(O(1)\\)

    In practical applications, we can directly use the heap class (or priority queue class) provided by programming languages.

    Similar to \"ascending order\" and \"descending order\" in sorting algorithms, we can implement conversion between \"min heap\" and \"max heap\" by setting a flag or modifying the Comparator. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py
    # Initialize a min heap\nmin_heap, flag = [], 1\n# Initialize a max heap\nmax_heap, flag = [], -1\n\n# Python's heapq module implements a min heap by default\n# Consider negating elements before pushing them to the heap, which inverts the size relationship and thus implements a max heap\n# In this example, flag = 1 corresponds to a min heap, flag = -1 corresponds to a max heap\n\n# Push elements into the heap\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# Get the heap top element\npeek: int = flag * max_heap[0] # 5\n\n# Remove the heap top element\n# The removed elements will form a descending sequence\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# Get the heap size\nsize: int = len(max_heap)\n\n# Check if the heap is empty\nis_empty: bool = not max_heap\n\n# Build a heap from an input list\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n
    heap.cpp
    /* Initialize a heap */\n// Initialize a min heap\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// Initialize a max heap\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* Push elements into the heap */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* Get the heap top element */\nint peek = maxHeap.top(); // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* Get the heap size */\nint size = maxHeap.size();\n\n/* Check if the heap is empty */\nbool isEmpty = maxHeap.empty();\n\n/* Build a heap from an input list */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n
    heap.java
    /* Initialize a heap */\n// Initialize a min heap\nQueue<Integer> minHeap = new PriorityQueue<>();\n// Initialize a max heap (use lambda expression to modify Comparator)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* Push elements into the heap */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* Get the heap top element */\nint peek = maxHeap.peek(); // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* Get the heap size */\nint size = maxHeap.size();\n\n/* Check if the heap is empty */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* Build a heap from an input list */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n
    heap.cs
    /* Initialize a heap */\n// Initialize a min heap\nPriorityQueue<int, int> minHeap = new();\n// Initialize a max heap (use lambda expression to modify Comparer)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y.CompareTo(x)));\n\n/* Push elements into the heap */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* Get the heap top element */\nint peek = maxHeap.Peek();//5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.Dequeue();  // 5\npeek = maxHeap.Dequeue();  // 4\npeek = maxHeap.Dequeue();  // 3\npeek = maxHeap.Dequeue();  // 2\npeek = maxHeap.Dequeue();  // 1\n\n/* Get the heap size */\nint size = maxHeap.Count;\n\n/* Check if the heap is empty */\nbool isEmpty = maxHeap.Count == 0;\n\n/* Build a heap from an input list */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n
    heap.go
    // In Go, we can construct a max heap of integers by implementing heap.Interface\n// Implementing heap.Interface also requires implementing sort.Interface\ntype intHeap []any\n\n// Push implements the heap.Interface method for pushing an element into the heap\nfunc (h *intHeap) Push(x any) {\n    // Push and Pop use pointer receiver as parameters\n    // because they not only adjust the slice contents but also modify the slice length\n    *h = append(*h, x.(int))\n}\n\n// Pop implements the heap.Interface method for popping the heap top element\nfunc (h *intHeap) Pop() any {\n    // The element to be removed is stored at the end\n    last := (*h)[len(*h)-1]\n    *h = (*h)[:len(*h)-1]\n    return last\n}\n\n// Len is a sort.Interface method\nfunc (h *intHeap) Len() int {\n    return len(*h)\n}\n\n// Less is a sort.Interface method\nfunc (h *intHeap) Less(i, j int) bool {\n    // To implement a min heap, change this to a less-than sign\n    return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// Swap is a sort.Interface method\nfunc (h *intHeap) Swap(i, j int) {\n    (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top gets the heap top element\nfunc (h *intHeap) Top() any {\n    return (*h)[0]\n}\n\n/* Driver Code */\nfunc TestHeap(t *testing.T) {\n    /* Initialize a heap */\n    // Initialize a max heap\n    maxHeap := &intHeap{}\n    heap.Init(maxHeap)\n    /* Push elements into the heap */\n    // Call heap.Interface methods to add elements\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    /* Get the heap top element */\n    top := maxHeap.Top()\n    fmt.Printf(\"Heap top element is %d\\n\", top)\n\n    /* Remove the heap top element */\n    // Call heap.Interface methods to remove elements\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    /* Get the heap size */\n    size := len(*maxHeap)\n    fmt.Printf(\"Number of heap elements is %d\\n\", size)\n\n    /* Check if the heap is empty */\n    isEmpty := len(*maxHeap) == 0\n    fmt.Printf(\"Is the heap empty? %t\\n\", isEmpty)\n}\n
    heap.swift
    /* Initialize a heap */\n// Swift's Heap type supports both max heaps and min heaps, and requires importing swift-collections\nvar heap = Heap<Int>()\n\n/* Push elements into the heap */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* Get the heap top element */\nvar peek = heap.max()!\n\n/* Remove the heap top element */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* Get the heap size */\nlet size = heap.count\n\n/* Check if the heap is empty */\nlet isEmpty = heap.isEmpty\n\n/* Build a heap from an input list */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n
    heap.js
    // JavaScript does not provide a built-in Heap class\n
    heap.ts
    // TypeScript does not provide a built-in Heap class\n
    heap.dart
    // Dart does not provide a built-in Heap class\n
    heap.rs
    use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* Initialize a heap */\n// Initialize a min heap\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// Initialize a max heap\nlet mut max_heap = BinaryHeap::new();\n\n/* Push elements into the heap */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* Get the heap top element */\nlet peek = max_heap.peek().unwrap();  // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\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/* Get the heap size */\nlet size = max_heap.len();\n\n/* Check if the heap is empty */\nlet is_empty = max_heap.is_empty();\n\n/* Build a heap from an input list */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n
    heap.c
    // C does not provide a built-in Heap class\n
    heap.kt
    /* Initialize a heap */\n// Initialize a min heap\nvar minHeap = PriorityQueue<Int>()\n// Initialize a max heap (use lambda expression to modify Comparator)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* Push elements into the heap */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* Get the heap top element */\nvar peek = maxHeap.peek() // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* Get the heap size */\nval size = maxHeap.size\n\n/* Check if the heap is empty */\nval isEmpty = maxHeap.isEmpty()\n\n/* Build a heap from an input list */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n
    heap.rb
    # Ruby does not provide a built-in Heap class\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#812-implementation-of-the-heap","level":2,"title":"8.1.2   Implementation of the Heap","text":"

    The following implementation is for a max heap. To convert it to a min heap, simply reverse all comparison logic related to ordering (for example, replace \\(\\geq\\) with \\(\\leq\\)). Interested readers are encouraged to implement this on their own.

    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#1-heap-storage-and-representation","level":3,"title":"1.   Heap Storage and Representation","text":"

    As mentioned in the \"Binary Tree\" chapter, complete binary trees are well-suited for array representation. Since heaps are a type of complete binary tree, we will use arrays to store heaps.

    When representing a binary tree with an array, elements represent node values, and indexes represent node positions in the binary tree. Parent-child relationships are represented through index-mapping formulas.

    As shown in Figure 8-2, given an index \\(i\\), the index of its left child is \\(2i + 1\\), the index of its right child is \\(2i + 2\\), and the index of its parent is \\((i - 1) / 2\\) (floor division). When an index is out of bounds, it indicates a null node or that the node does not exist.

    Figure 8-2   Representation and storage of heaps

    We can encapsulate the index mapping formula into functions for convenient subsequent use:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def left(self, i: int) -> int:\n    \"\"\"Get index of left child node\"\"\"\n    return 2 * i + 1\n\ndef right(self, i: int) -> int:\n    \"\"\"Get index of right child node\"\"\"\n    return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n    \"\"\"Get index of parent node\"\"\"\n    return (i - 1) // 2  # Floor division\n
    my_heap.cpp
    /* Get index of left child node */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
    my_heap.java
    /* Get index of left child node */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
    my_heap.cs
    /* Get index of left child node */\nint Left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint Right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint Parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
    my_heap.go
    /* Get index of left child node */\nfunc (h *maxHeap) left(i int) int {\n    return 2*i + 1\n}\n\n/* Get index of right child node */\nfunc (h *maxHeap) right(i int) int {\n    return 2*i + 2\n}\n\n/* Get index of parent node */\nfunc (h *maxHeap) parent(i int) int {\n    // Floor division\n    return (i - 1) / 2\n}\n
    my_heap.swift
    /* Get index of left child node */\nfunc left(i: Int) -> Int {\n    2 * i + 1\n}\n\n/* Get index of right child node */\nfunc right(i: Int) -> Int {\n    2 * i + 2\n}\n\n/* Get index of parent node */\nfunc parent(i: Int) -> Int {\n    (i - 1) / 2 // Floor division\n}\n
    my_heap.js
    /* Get index of left child node */\n#left(i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\n#right(i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\n#parent(i) {\n    return Math.floor((i - 1) / 2); // Floor division\n}\n
    my_heap.ts
    /* Get index of left child node */\nleft(i: number): number {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nright(i: number): number {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nparent(i: number): number {\n    return Math.floor((i - 1) / 2); // Floor division\n}\n
    my_heap.dart
    /* Get index of left child node */\nint _left(int i) {\n  return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint _right(int i) {\n  return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint _parent(int i) {\n  return (i - 1) ~/ 2; // Floor division\n}\n
    my_heap.rs
    /* Get index of left child node */\nfn left(i: usize) -> usize {\n    2 * i + 1\n}\n\n/* Get index of right child node */\nfn right(i: usize) -> usize {\n    2 * i + 2\n}\n\n/* Get index of parent node */\nfn parent(i: usize) -> usize {\n    (i - 1) / 2 // Floor division\n}\n
    my_heap.c
    /* Get index of left child node */\nint left(MaxHeap *maxHeap, int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(MaxHeap *maxHeap, int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(MaxHeap *maxHeap, int i) {\n    return (i - 1) / 2; // Round down\n}\n
    my_heap.kt
    /* Get index of left child node */\nfun left(i: Int): Int {\n    return 2 * i + 1\n}\n\n/* Get index of right child node */\nfun right(i: Int): Int {\n    return 2 * i + 2\n}\n\n/* Get index of parent node */\nfun parent(i: Int): Int {\n    return (i - 1) / 2 // Floor division\n}\n
    my_heap.rb
    ### Get left child index ###\ndef left(i)\n  2 * i + 1\nend\n\n### Get right child index ###\ndef right(i)\n  2 * i + 2\nend\n\n### Get parent node index ###\ndef parent(i)\n  (i - 1) / 2     # Floor division\nend\n
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#2-accessing-the-heap-top-element","level":3,"title":"2.   Accessing the Heap Top Element","text":"

    The heap top element is the root node of the binary tree, which is also the first element of the list:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def peek(self) -> int:\n    \"\"\"Access top element\"\"\"\n    return self.max_heap[0]\n
    my_heap.cpp
    /* Access top element */\nint peek() {\n    return maxHeap[0];\n}\n
    my_heap.java
    /* Access top element */\nint peek() {\n    return maxHeap.get(0);\n}\n
    my_heap.cs
    /* Access top element */\nint Peek() {\n    return maxHeap[0];\n}\n
    my_heap.go
    /* Access top element */\nfunc (h *maxHeap) peek() any {\n    return h.data[0]\n}\n
    my_heap.swift
    /* Access top element */\nfunc peek() -> Int {\n    maxHeap[0]\n}\n
    my_heap.js
    /* Access top element */\npeek() {\n    return this.#maxHeap[0];\n}\n
    my_heap.ts
    /* Access top element */\npeek(): number {\n    return this.maxHeap[0];\n}\n
    my_heap.dart
    /* Access top element */\nint peek() {\n  return _maxHeap[0];\n}\n
    my_heap.rs
    /* Access top element */\nfn peek(&self) -> Option<i32> {\n    self.max_heap.first().copied()\n}\n
    my_heap.c
    /* Access top element */\nint peek(MaxHeap *maxHeap) {\n    return maxHeap->data[0];\n}\n
    my_heap.kt
    /* Access top element */\nfun peek(): Int {\n    return maxHeap[0]\n}\n
    my_heap.rb
    ### Access heap top element ###\ndef peek\n  @max_heap[0]\nend\n
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#3-inserting-an-element-into-the-heap","level":3,"title":"3.   Inserting an Element Into the Heap","text":"

    Given an element val, we first add it to the bottom of the heap. After insertion, because val may be larger than other elements in the heap, the heap property may be violated. Therefore, we need to restore the heap property along the path from the inserted node to the root. This operation is called heapify.

    Starting from the inserted node, perform heapify from bottom to top. As shown in Figure 8-3, we compare the inserted node with its parent, and if the inserted node is larger, we swap them. We continue this process from bottom to top until we move past the root or reach a node that no longer needs to be swapped.

    <1><2><3><4><5><6><7><8><9>

    Figure 8-3   Steps of inserting an element into the heap

    Given a total of \\(n\\) nodes, the tree height is \\(O(\\log n)\\). Thus, the number of loop iterations in the heapify operation is at most \\(O(\\log n)\\), making the time complexity of the element insertion operation \\(O(\\log n)\\). The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def push(self, val: int):\n    \"\"\"Element enters heap\"\"\"\n    # Add node\n    self.max_heap.append(val)\n    # Heapify from bottom to top\n    self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n    \"\"\"Starting from node i, heapify from bottom to top\"\"\"\n    while True:\n        # Get parent node of node i\n        p = self.parent(i)\n        # When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # Swap two nodes\n        self.swap(i, p)\n        # Loop upward heapify\n        i = p\n
    my_heap.cpp
    /* Element enters heap */\nvoid push(int val) {\n    // Add node\n    maxHeap.push_back(val);\n    // Heapify from bottom to top\n    siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Swap two nodes\n        swap(maxHeap[i], maxHeap[p]);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.java
    /* Element enters heap */\nvoid push(int val) {\n    // Add node\n    maxHeap.add(val);\n    // Heapify from bottom to top\n    siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // Swap two nodes\n        swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.cs
    /* Element enters heap */\nvoid Push(int val) {\n    // Add node\n    maxHeap.Add(val);\n    // Heapify from bottom to top\n    SiftUp(Size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid SiftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = Parent(i);\n        // If 'past root node' or 'node needs no repair', end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Swap two nodes\n        Swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.go
    /* Element enters heap */\nfunc (h *maxHeap) push(val any) {\n    // Add node\n    h.data = append(h.data, val)\n    // Heapify from bottom to top\n    h.siftUp(len(h.data) - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfunc (h *maxHeap) siftUp(i int) {\n    for true {\n        // Get parent node of node i\n        p := h.parent(i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // Swap two nodes\n        h.swap(i, p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
    my_heap.swift
    /* Element enters heap */\nfunc push(val: Int) {\n    // Add node\n    maxHeap.append(val)\n    // Heapify from bottom to top\n    siftUp(i: size() - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfunc siftUp(i: Int) {\n    var i = i\n    while true {\n        // Get parent node of node i\n        let p = parent(i: i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // Swap two nodes\n        swap(i: i, j: p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
    my_heap.js
    /* Element enters heap */\npush(val) {\n    // Add node\n    this.#maxHeap.push(val);\n    // Heapify from bottom to top\n    this.#siftUp(this.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\n#siftUp(i) {\n    while (true) {\n        // Get parent node of node i\n        const p = this.#parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // Swap two nodes\n        this.#swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.ts
    /* Element enters heap */\npush(val: number): void {\n    // Add node\n    this.maxHeap.push(val);\n    // Heapify from bottom to top\n    this.siftUp(this.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nsiftUp(i: number): void {\n    while (true) {\n        // Get parent node of node i\n        const p = this.parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // Swap two nodes\n        this.swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.dart
    /* Element enters heap */\nvoid push(int val) {\n  // Add node\n  _maxHeap.add(val);\n  // Heapify from bottom to top\n  siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n  while (true) {\n    // Get parent node of node i\n    int p = _parent(i);\n    // When \"crossing root node\" or \"node needs no repair\", end heapify\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // Swap two nodes\n    _swap(i, p);\n    // Loop upward heapify\n    i = p;\n  }\n}\n
    my_heap.rs
    /* Element enters heap */\nfn push(&mut self, val: i32) {\n    // Add node\n    self.max_heap.push(val);\n    // Heapify from bottom to top\n    self.sift_up(self.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nfn sift_up(&mut self, mut i: usize) {\n    loop {\n        // Node i is already the heap root, end heapification\n        if i == 0 {\n            break;\n        }\n        // Get parent node of node i\n        let p = Self::parent(i);\n        // When \"node needs no repair\", end heapification\n        if self.max_heap[i] <= self.max_heap[p] {\n            break;\n        }\n        // Swap two nodes\n        self.swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.c
    /* Element enters heap */\nvoid push(MaxHeap *maxHeap, int val) {\n    // By default, should not add this many nodes\n    if (maxHeap->size == MAX_SIZE) {\n        printf(\"heap is full!\");\n        return;\n    }\n    // Add node\n    maxHeap->data[maxHeap->size] = val;\n    maxHeap->size++;\n\n    // Heapify from bottom to top\n    siftUp(maxHeap, maxHeap->size - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(maxHeap, i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // Swap two nodes\n        swap(maxHeap, i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
    my_heap.kt
    /* Element enters heap */\nfun push(_val: Int) {\n    // Add node\n    maxHeap.add(_val)\n    // Heapify from bottom to top\n    siftUp(size() - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfun siftUp(it: Int) {\n    // Kotlin function parameters are immutable, so create temporary variable\n    var i = it\n    while (true) {\n        // Get parent node of node i\n        val p = parent(i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // Swap two nodes\n        swap(i, p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
    my_heap.rb
    ### Push element to heap ###\ndef push(val)\n  # Add node\n  @max_heap << val\n  # Heapify from bottom to top\n  sift_up(size - 1)\nend\n\n### Heapify from node i, bottom to top ###\ndef sift_up(i)\n  loop do\n    # Get parent node of node i\n    p = parent(i)\n    # When \"crossing root node\" or \"node needs no repair\", end heapify\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # Swap two nodes\n    swap(i, p)\n    # Loop upward heapify\n    i = p\n  end\nend\n
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#4-removing-the-heap-top-element","level":3,"title":"4.   Removing the Heap Top Element","text":"

    The heap top element is the root node of the binary tree, which is the first element of the list. If we directly remove the first element from the list, all node indexes in the binary tree would change, making subsequent repair with heapify difficult. To minimize changes in element indexes, we use the following steps.

    1. Swap the heap top element with the heap bottom element (swap the root node with the rightmost leaf node).
    2. After swapping, remove the heap bottom from the list (note that since we've swapped, we're actually removing the original heap top element).
    3. Starting from the root node, perform heapify from top to bottom.

    As shown in Figure 8-4, the direction of \"top-to-bottom heapify\" is opposite to \"bottom-to-top heapify\". We compare the root node's value with its two children and swap it with the largest child. Then loop this operation until we pass a leaf node or encounter a node that doesn't need swapping.

    <1><2><3><4><5><6><7><8><9><10>

    Figure 8-4   Steps of removing the heap top element

    Similar to the element insertion operation, the time complexity of the heap top element removal operation is also \\(O(\\log n)\\). The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def pop(self) -> int:\n    \"\"\"Element exits heap\"\"\"\n    # Handle empty case\n    if self.is_empty():\n        raise IndexError(\"Heap is empty\")\n    # Swap root node with rightmost leaf node (swap first element with last element)\n    self.swap(0, self.size() - 1)\n    # Delete node\n    val = self.max_heap.pop()\n    # Heapify from top to bottom\n    self.sift_down(0)\n    # Return top element\n    return val\n\ndef sift_down(self, i: int):\n    \"\"\"Starting from node i, heapify from top to bottom\"\"\"\n    while True:\n        # Find node with largest value among i, l, r, denoted as 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        # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        if ma == i:\n            break\n        # Swap two nodes\n        self.swap(i, ma)\n        # Loop downward heapify\n        i = ma\n
    my_heap.cpp
    /* Element exits heap */\nvoid pop() {\n    // Handle empty case\n    if (isEmpty()) {\n        throw out_of_range(\"Heap is empty\");\n    }\n    // Delete node\n    swap(maxHeap[0], maxHeap[size() - 1]);\n    // Remove node\n    maxHeap.pop_back();\n    // Return top element\n    siftDown(0);\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i)\n            break;\n        swap(maxHeap[i], maxHeap[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.java
    /* Element exits heap */\nint pop() {\n    // Handle empty case\n    if (isEmpty())\n        throw new IndexOutOfBoundsException();\n    // Delete node\n    swap(0, size() - 1);\n    // Remove node\n    int val = maxHeap.remove(size() - 1);\n    // Return top element\n    siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.cs
    /* Element exits heap */\nint Pop() {\n    // Handle empty case\n    if (IsEmpty())\n        throw new IndexOutOfRangeException();\n    // Delete node\n    Swap(0, Size() - 1);\n    // Remove node\n    int val = maxHeap.Last();\n    maxHeap.RemoveAt(Size() - 1);\n    // Return top element\n    SiftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid SiftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // If 'node i is largest' or 'past leaf node', end heapify\n        if (ma == i) break;\n        // Swap two nodes\n        Swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.go
    /* Element exits heap */\nfunc (h *maxHeap) pop() any {\n    // Handle empty case\n    if h.isEmpty() {\n        fmt.Println(\"error\")\n        return nil\n    }\n    // Delete node\n    h.swap(0, h.size()-1)\n    // Remove node\n    val := h.data[len(h.data)-1]\n    h.data = h.data[:len(h.data)-1]\n    // Return top element\n    h.siftDown(0)\n\n    // Return heap top element\n    return val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfunc (h *maxHeap) siftDown(i int) {\n    for true {\n        // Find node with maximum value among nodes i, l, r, denoted as 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        // Swap two nodes\n        if max == i {\n            break\n        }\n        // Swap two nodes\n        h.swap(i, max)\n        // Loop downwards heapification\n        i = max\n    }\n}\n
    my_heap.swift
    /* Element exits heap */\nfunc pop() -> Int {\n    // Handle empty case\n    if isEmpty() {\n        fatalError(\"Heap is empty\")\n    }\n    // Delete node\n    swap(i: 0, j: size() - 1)\n    // Remove node\n    let val = maxHeap.remove(at: size() - 1)\n    // Return top element\n    siftDown(i: 0)\n    // Return heap top element\n    return val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfunc siftDown(i: Int) {\n    var i = i\n    while true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        swap(i: i, j: ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n
    my_heap.js
    /* Element exits heap */\npop() {\n    // Handle empty case\n    if (this.isEmpty()) throw new Error('Heap is empty');\n    // Delete node\n    this.#swap(0, this.size() - 1);\n    // Remove node\n    const val = this.#maxHeap.pop();\n    // Return top element\n    this.#siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\n#siftDown(i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma === i) break;\n        // Swap two nodes\n        this.#swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.ts
    /* Element exits heap */\npop(): number {\n    // Handle empty case\n    if (this.isEmpty()) throw new RangeError('Heap is empty.');\n    // Delete node\n    this.swap(0, this.size() - 1);\n    // Remove node\n    const val = this.maxHeap.pop();\n    // Return top element\n    this.siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nsiftDown(i: number): void {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma === i) break;\n        // Swap two nodes\n        this.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.dart
    /* Element exits heap */\nint pop() {\n  // Handle empty case\n  if (isEmpty()) throw Exception('Heap is empty');\n  // Delete node\n  _swap(0, size() - 1);\n  // Remove node\n  int val = _maxHeap.removeLast();\n  // Return top element\n  siftDown(0);\n  // Return heap top element\n  return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n  while (true) {\n    // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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    // Swap two nodes\n    if (ma == i) break;\n    // Swap two nodes\n    _swap(i, ma);\n    // Loop downwards heapification\n    i = ma;\n  }\n}\n
    my_heap.rs
    /* Element exits heap */\nfn pop(&mut self) -> i32 {\n    // Handle empty case\n    if self.is_empty() {\n        panic!(\"index out of bounds\");\n    }\n    // Delete node\n    self.swap(0, self.size() - 1);\n    // Remove node\n    let val = self.max_heap.pop().unwrap();\n    // Return top element\n    self.sift_down(0);\n    // Return heap top element\n    val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfn sift_down(&mut self, mut i: usize) {\n    loop {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break;\n        }\n        // Swap two nodes\n        self.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
    my_heap.c
    /* Element exits heap */\nint pop(MaxHeap *maxHeap) {\n    // Handle empty case\n    if (isEmpty(maxHeap)) {\n        printf(\"heap is empty!\");\n        return INT_MAX;\n    }\n    // Delete node\n    swap(maxHeap, 0, size(maxHeap) - 1);\n    // Remove node\n    int val = maxHeap->data[maxHeap->size - 1];\n    maxHeap->size--;\n    // Return top element\n    siftDown(maxHeap, 0);\n\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Find node with maximum value among nodes i, l, r, denoted as 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        // Swap two nodes\n        if (max == i) {\n            break;\n        }\n        // Swap two nodes\n        swap(maxHeap, i, max);\n        // Loop downwards heapification\n        i = max;\n    }\n}\n
    my_heap.kt
    /* Element exits heap */\nfun pop(): Int {\n    // Handle empty case\n    if (isEmpty()) throw IndexOutOfBoundsException()\n    // Delete node\n    swap(0, size() - 1)\n    // Remove node\n    val _val = maxHeap.removeAt(size() - 1)\n    // Return top element\n    siftDown(0)\n    // Return heap top element\n    return _val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfun siftDown(it: Int) {\n    // Kotlin function parameters are immutable, so create temporary variable\n    var i = it\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i) break\n        // Swap two nodes\n        swap(i, ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n
    my_heap.rb
    ### Pop element from heap ###\ndef pop\n  # Handle empty case\n  raise IndexError, \"Heap is empty\" if is_empty?\n  # Delete node\n  swap(0, size - 1)\n  # Remove node\n  val = @max_heap.pop\n  # Return top element\n  sift_down(0)\n  # Return heap top element\n  val\nend\n\n### Heapify from node i, top to bottom ###\ndef sift_down(i)\n  loop do\n    # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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    # Swap two nodes\n    break if ma == i\n\n    # Swap two nodes\n    swap(i, ma)\n    # Loop downwards heapification\n    i = ma\n  end\nend\n
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#813-common-applications-of-heaps","level":2,"title":"8.1.3   Common Applications of Heaps","text":"
    • Priority queue: Heaps are typically the preferred data structure for implementing priority queues. The time complexity of both enqueue and dequeue operations is \\(O(\\log n)\\), and heap construction has a time complexity of \\(O(n)\\), making these operations highly efficient.
    • Heap sort: Given a set of data, we can build a heap with them and then continuously perform element removal operations to obtain sorted data. However, we usually use a more elegant approach to implement heap sort, as detailed in the \"Heap Sort\" chapter.
    • Getting the largest \\(k\\) elements: This is a classic algorithm problem and also a typical application, such as selecting the top 10 trending news items for Weibo Hot Search or the top 10 best-selling products.
    ","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   Summary","text":"","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • A heap is a complete binary tree. Depending on the property it satisfies, it can be classified as either a max heap or a min heap. The top element of a max heap (min heap) is the largest (smallest) element.
    • A priority queue is a queue in which elements are dequeued according to priority, and it is typically implemented using a heap.
    • Common heap operations and their corresponding time complexities include inserting an element \\(O(\\log n)\\), removing the top element \\(O(\\log n)\\), and accessing the top element \\(O(1)\\).
    • Complete binary trees are well-suited for array representation, so we typically use arrays to store heaps.
    • Heapify operations are used to maintain the heap property and are employed in both element insertion and removal operations.
    • Building a heap from \\(n\\) input elements can be optimized to \\(O(n)\\), which is highly efficient.
    • Top-k is a classic algorithmic problem that can be solved efficiently using a heap, with a time complexity of \\(O(n \\log k)\\).
    ","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Does the term \"heap\" in data structures mean the same thing as \"heap\" in memory management?

    They are not the same concept; they simply share the same name. In computer systems, the heap is part of dynamic memory allocation, and programs can use it to store data at runtime. A program can request a certain amount of heap memory to store complex structures such as objects and arrays. When the data is no longer needed, the program must release that memory to prevent memory leaks. Compared with stack memory, heap memory requires more careful management and use; improper handling can lead to problems such as memory leaks and dangling pointers.

    ","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Top-k Problem","text":"

    Question

    Given an unordered array nums of length \\(n\\), return the largest \\(k\\) elements in the array.

    For this problem, we will first introduce two relatively straightforward solutions, followed by a more efficient heap-based solution.

    ","path":["Chapter 8. Heap","8.3   Top-k Problem"],"tags":[]},{"location":"chapter_heap/top_k/#831-method-1-iterative-selection","level":2,"title":"8.3.1   Method 1: Iterative Selection","text":"

    We can perform \\(k\\) rounds of traversal as shown in Figure 8-6, extracting the \\(1^{st}\\), \\(2^{nd}\\), \\(\\dots\\), \\(k^{th}\\) largest elements in each round, with a time complexity of \\(O(nk)\\).

    This method is only suitable when \\(k \\ll n\\), because when \\(k\\) is close to \\(n\\), the time complexity approaches \\(O(n^2)\\), making it very inefficient.

    Figure 8-6   Traversing to find the largest k elements

    Tip

    When \\(k = n\\), we can obtain a complete sorted sequence, which is equivalent to the \"selection sort\" algorithm.

    ","path":["Chapter 8. Heap","8.3   Top-k Problem"],"tags":[]},{"location":"chapter_heap/top_k/#832-method-2-sorting","level":2,"title":"8.3.2   Method 2: Sorting","text":"

    As shown in Figure 8-7, we can first sort the array nums, then return the rightmost \\(k\\) elements, with a time complexity of \\(O(n \\log n)\\).

    Clearly, this method does more work than necessary, because we only need to find the largest \\(k\\) elements rather than sort the other elements.

    Figure 8-7   Sorting to find the largest k elements

    ","path":["Chapter 8. Heap","8.3   Top-k Problem"],"tags":[]},{"location":"chapter_heap/top_k/#833-method-3-heap","level":2,"title":"8.3.3   Method 3: Heap","text":"

    We can solve the Top-k problem more efficiently with a heap, as shown in Figure 8-8.

    1. Initialize a min heap, where the heap top element is the smallest.
    2. First, insert the first \\(k\\) elements of the array into the heap in sequence.
    3. Starting from the \\((k + 1)^{th}\\) element, if the current element is greater than the heap top element, remove the heap top element and insert the current element into the heap.
    4. After traversal is complete, the heap contains the largest \\(k\\) elements.
    <1><2><3><4><5><6><7><8><9>

    Figure 8-8   Finding the largest k elements using a heap

    Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py
    def top_k_heap(nums: list[int], k: int) -> list[int]:\n    \"\"\"Find the largest k elements in array based on heap\"\"\"\n    # Initialize min heap\n    heap = []\n    # Enter the first k elements of array into heap\n    for i in range(k):\n        heapq.heappush(heap, nums[i])\n    # Starting from the (k+1)th element, maintain heap length as k\n    for i in range(k, len(nums)):\n        # If current element is greater than top element, top element exits heap, current element enters heap\n        if nums[i] > heap[0]:\n            heapq.heappop(heap)\n            heapq.heappush(heap, nums[i])\n    return heap\n
    top_k.cpp
    /* Find the largest k elements in array based on heap */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n    // Python's heapq module implements min heap by default\n    priority_queue<int, vector<int>, greater<int>> heap;\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.push(nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.size(); i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.top()) {\n            heap.pop();\n            heap.push(nums[i]);\n        }\n    }\n    return heap;\n}\n
    top_k.java
    /* Find the largest k elements in array based on heap */\nQueue<Integer> topKHeap(int[] nums, int k) {\n    // Python's heapq module implements min heap by default\n    Queue<Integer> heap = new PriorityQueue<Integer>();\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.offer(nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.peek()) {\n            heap.poll();\n            heap.offer(nums[i]);\n        }\n    }\n    return heap;\n}\n
    top_k.cs
    /* Find the largest k elements in array based on heap */\nPriorityQueue<int, int> TopKHeap(int[] nums, int k) {\n    // Python's heapq module implements min heap by default\n    PriorityQueue<int, int> heap = new();\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.Enqueue(nums[i], nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.Length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\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
    /* Find the largest k elements in array based on heap */\nfunc topKHeap(nums []int, k int) *minHeap {\n    // Python's heapq module implements min heap by default\n    h := &minHeap{}\n    heap.Init(h)\n    // Enter the first k elements of array into heap\n    for i := 0; i < k; i++ {\n        heap.Push(h, nums[i])\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for i := k; i < len(nums); i++ {\n        // If current element is greater than top element, top element exits heap, current element enters heap\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
    /* Find the largest k elements in array based on heap */\nfunc topKHeap(nums: [Int], k: Int) -> [Int] {\n    // Initialize min heap and build heap with first k elements\n    var heap = Heap(nums.prefix(k))\n    // Starting from the (k+1)th element, maintain heap length as k\n    for i in nums.indices.dropFirst(k) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\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
    /* Element enters heap */\nfunction pushMinHeap(maxHeap, val) {\n    // Negate element\n    maxHeap.push(-val);\n}\n\n/* Element exits heap */\nfunction popMinHeap(maxHeap) {\n    // Negate element\n    return -maxHeap.pop();\n}\n\n/* Access top element */\nfunction peekMinHeap(maxHeap) {\n    // Negate element\n    return -maxHeap.peek();\n}\n\n/* Extract elements from heap */\nfunction getMinHeap(maxHeap) {\n    // Negate element\n    return maxHeap.getMaxHeap().map((num) => -num);\n}\n\n/* Find the largest k elements in array based on heap */\nfunction topKHeap(nums, k) {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    const maxHeap = new MaxHeap([]);\n    // Enter the first k elements of array into heap\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (let i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Return elements in heap\n    return getMinHeap(maxHeap);\n}\n
    top_k.ts
    /* Element enters heap */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n    // Negate element\n    maxHeap.push(-val);\n}\n\n/* Element exits heap */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n    // Negate element\n    return -maxHeap.pop();\n}\n\n/* Access top element */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n    // Negate element\n    return -maxHeap.peek();\n}\n\n/* Extract elements from heap */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n    // Negate element\n    return maxHeap.getMaxHeap().map((num: number) => -num);\n}\n\n/* Find the largest k elements in array based on heap */\nfunction topKHeap(nums: number[], k: number): number[] {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    const maxHeap = new MaxHeap([]);\n    // Enter the first k elements of array into heap\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (let i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Return elements in heap\n    return getMinHeap(maxHeap);\n}\n
    top_k.dart
    /* Find the largest k elements in array based on heap */\nMinHeap topKHeap(List<int> nums, int k) {\n  // Initialize min heap, push first k elements of array to heap\n  MinHeap heap = MinHeap(nums.sublist(0, k));\n  // Starting from the (k+1)th element, maintain heap length as k\n  for (int i = k; i < nums.length; i++) {\n    // If current element is greater than top element, top element exits heap, current element enters heap\n    if (nums[i] > heap.peek()) {\n      heap.pop();\n      heap.push(nums[i]);\n    }\n  }\n  return heap;\n}\n
    top_k.rs
    /* Find the largest k elements in array based on heap */\nfn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {\n    // BinaryHeap is a max heap, use Reverse to negate elements to implement min heap\n    let mut heap = BinaryHeap::<Reverse<i32>>::new();\n    // Enter the first k elements of array into heap\n    for &num in nums.iter().take(k) {\n        heap.push(Reverse(num));\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for &num in nums.iter().skip(k) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if num > heap.peek().unwrap().0 {\n            heap.pop();\n            heap.push(Reverse(num));\n        }\n    }\n    heap\n}\n
    top_k.c
    /* Element enters heap */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n    // Negate element\n    push(maxHeap, -val);\n}\n\n/* Element exits heap */\nint popMinHeap(MaxHeap *maxHeap) {\n    // Negate element\n    return -pop(maxHeap);\n}\n\n/* Access top element */\nint peekMinHeap(MaxHeap *maxHeap) {\n    // Negate element\n    return -peek(maxHeap);\n}\n\n/* Extract elements from heap */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Negate all heap elements and store in res array\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/* Extract elements from heap */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Negate all heap elements and store in res array\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// Function to find k largest elements in array using heap\nint *topKHeap(int *nums, int sizeNums, int k) {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    int *empty = (int *)malloc(0);\n    MaxHeap *maxHeap = newMaxHeap(empty, 0);\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < sizeNums; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    int *res = getMinHeap(maxHeap);\n    // Free memory\n    delMaxHeap(maxHeap);\n    return res;\n}\n
    top_k.kt
    /* Find the largest k elements in array based on heap */\nfun topKHeap(nums: IntArray, k: Int): Queue<Int> {\n    // Python's heapq module implements min heap by default\n    val heap = PriorityQueue<Int>()\n    // Enter the first k elements of array into heap\n    for (i in 0..<k) {\n        heap.offer(nums[i])\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (i in k..<nums.size) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.peek()) {\n            heap.poll()\n            heap.offer(nums[i])\n        }\n    }\n    return heap\n}\n
    top_k.rb
    ### Find largest k elements in array using heap ###\ndef top_k_heap(nums, k)\n  # Python's heapq module implements min heap by default\n  # Note: We negate all heap elements to simulate min heap using max heap\n  max_heap = MaxHeap.new([])\n\n  # Enter the first k elements of array into heap\n  for i in 0...k\n    push_min_heap(max_heap, nums[i])\n  end\n\n  # Starting from the (k+1)th element, maintain heap length as k\n  for i in k...nums.length\n    # If current element is greater than top element, top element exits heap, current element enters heap\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

    A total of \\(n\\) rounds of heap insertions and removals are performed, with the heap's maximum length being \\(k\\), so the time complexity is \\(O(n \\log k)\\). This method is very efficient; when \\(k\\) is small, the time complexity approaches \\(O(n)\\); when \\(k\\) is large, the time complexity does not exceed \\(O(n \\log n)\\).

    Additionally, this method is well suited to dynamic data streams. As new data arrives, we can continuously maintain the elements in the heap, enabling dynamic updates to the largest \\(k\\) elements.

    ","path":["Chapter 8. Heap","8.3   Top-k Problem"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"Preface","text":"

    A few years ago, I shared the \"Sword for Offer\" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most frequently asked question I encountered was \"how to get started with algorithms.\" Gradually, I developed a keen interest in this question.

    Diving straight into problem-solving seems to be the most popular approach—it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-learning abilities can successfully defuse the mines one by one, while those with insufficient foundations may end up bruised and battered, retreating step by step in frustration. Reading through textbooks is also a common practice, but for job seekers, graduation theses, resume submissions, and preparations for written tests and interviews have already consumed most of their energy, making working through thick books an arduous challenge.

    If you're facing similar struggles, then it's fortunate that this book has \"found\" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you through the \"landscape\" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different \"mines,\" and enable you to master various \"mine-clearing methods.\" With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system.

    I deeply agree with Professor Feynman's words: \"Knowledge isn't free. You have to pay attention.\" In this sense, this book is not entirely \"free.\" In order to live up to the precious \"attention\" you invest in this book, I will do my utmost and devote my greatest \"attention\" to completing this work.

    I'm keenly aware of the limits of my knowledge and experience. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students.

    Hello, Algorithms!

    The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all striking demonstrations of algorithms at work on computers.

    In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms gradually became more refined and complex. From the ingenious craftsmanship of master artisans, to industrial products that liberate productive forces, to the scientific laws governing the operation of the universe, behind almost every ordinary or astonishing thing lies ingenious algorithmic thinking.

    Similarly, data structures are everywhere: from large-scale social networks to small subway systems, many systems can be modeled as \"graphs\"; from a nation to a family, the primary organizational forms of society exhibit characteristics of \"trees\"; winter clothing is like a \"stack,\" where the first item put on is the last to be taken off; a badminton tube is like a \"queue,\" with items inserted at one end and retrieved from the other; a dictionary is like a \"hash table,\" enabling quick lookup of target entries.

    This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them in code. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you!

    ","path":["Before Starting","Preface"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"Chapter 1.   Encounter with Algorithms","text":"

    Abstract

    A young girl dances gracefully, intertwined with data, her skirt flowing with the melody of algorithms.

    She invites you to dance with her. Follow her steps closely and enter the world of algorithms, full of logic and beauty.

    ","path":["Chapter 1. Encounter with Algorithms","Chapter 1.   Encounter with Algorithms"],"tags":[]},{"location":"chapter_introduction/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 1.1   Algorithms Are Everywhere
    • 1.2   What Is an Algorithm
    • 1.3   Summary
    ","path":["Chapter 1. Encounter with Algorithms","Chapter 1.   Encounter with Algorithms"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   Algorithms Are Everywhere","text":"

    When we hear the term \"algorithm,\" we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives.

    Before we formally explore algorithms, here's an interesting fact worth sharing: you have already learned many algorithms without realizing it, and you are used to applying them in daily life. Let me give a few specific examples to illustrate this point.

    Example 1: Looking Up a Dictionary. In an English dictionary, words are listed alphabetically. Assuming we're searching for a word that starts with the letter \\(r\\), this is typically done in the following way:

    1. Open the dictionary to about halfway and check the first word on that page; suppose it starts with the letter \\(m\\).
    2. Since \\(r\\) comes after \\(m\\) in the alphabet, the first half can be ignored and the search space is narrowed down to the second half.
    3. Repeat steps 1. and 2. until you find the page where the word starts with \\(r\\).
    <1><2><3><4><5>

    Figure 1-1   Process of looking up a dictionary

    Looking up a dictionary, an essential skill for elementary school students is actually the famous \"Binary Search\" algorithm. From a data structure perspective, we can consider the dictionary as a sorted \"array\"; from an algorithmic perspective, the series of actions taken to look up a word in the dictionary can be viewed as the algorithm \"Binary Search.\"

    Example 2: Organizing Playing Cards. When playing cards, we need to arrange the cards in our hands in ascending order, as shown in the following process.

    1. Divide the playing cards into \"ordered\" and \"unordered\" sections, assuming initially the leftmost card is already in order.
    2. Take out a card from the unordered section and insert it into the correct position in the ordered section; after this, the leftmost two cards are in order.
    3. Repeat step 2 until all cards are in order.

    Figure 1-2   Process of sorting a deck of cards

    The above method of organizing playing cards is essentially the \"Insertion Sort\" algorithm, which is very efficient for small datasets. Many programming languages' built-in sorting implementations use insertion sort internally.

    Example 3: Making Change. Assume making a purchase of \\(69\\) at a supermarket. If you give the cashier \\(100\\), they will need to provide you with \\(31\\) in change. This process can be clearly understood as illustrated in Figure 1-3.

    1. The available denominations smaller than \\(31\\) are \\(1\\), \\(5\\), \\(10\\), and \\(20\\).
    2. Take out the largest \\(20\\) from the options, leaving \\(31 - 20 = 11\\).
    3. Take out the largest \\(10\\) from the remaining options, leaving \\(11 - 10 = 1\\).
    4. Take out the largest \\(1\\) from the remaining options, leaving \\(1 - 1 = 0\\).
    5. Complete change-making, the solution is \\(20 + 10 + 1 = 31\\).

    Figure 1-3   Process of making change

    In the steps above, we choose what seems to be the best option at each stage by using the largest denomination available, which leads to an effective way to make change. From a data structures and algorithms perspective, this approach is known as a \"Greedy\" algorithm.

    From cooking a meal to interstellar travel, almost all problem-solving involves algorithms. The advent of computers allows us to store data structures in memory and write code to call the CPU and GPU to execute algorithms. In this way, we can transfer real-life problems to computers and solve various complex issues in a more efficient way.

    Tip

    If concepts such as data structures, algorithms, arrays, and binary search still feel only half-familiar, keep reading. This book will guide you into the world of data structures and algorithms.

    ","path":["Chapter 1. Encounter with Algorithms","1.1   Algorithms Are Everywhere"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   Summary","text":"","path":["Chapter 1. Encounter with Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Algorithms are ubiquitous in daily life and are not some distant, esoteric body of knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life.
    • The principle of looking up a dictionary is consistent with the binary search algorithm. Binary search embodies the important algorithmic idea of divide and conquer.
    • The process of organizing playing cards is very similar to the insertion sort algorithm. Insertion sort is suitable for sorting small datasets.
    • The steps of making change are essentially a greedy algorithm, where the best choice is made at each step based on the current situation.
    • An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is a way of organizing and storing data in a computer.
    • Data structures and algorithms are closely connected. Data structures are the foundation of algorithms, and algorithms breathe life into data structures.
    • We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the way they are shaped and connected represents the data structure, and the steps used to assemble them correspond to the algorithm.
    ","path":["Chapter 1. Encounter with Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: As a programmer, I have never used algorithms to solve problems in my daily work. Common algorithms are already encapsulated by programming languages and can be used directly. Does this mean that the problems in our work have not yet reached the level where algorithms are needed?

    If we compare specific work skills to \"techniques\" in martial arts, then fundamental subjects should be more like \"internal skills\".

    I believe the significance of learning algorithms (and other fundamental subjects) is not that you will need to implement them from scratch at work, but that the knowledge you gain enables you to make sound professional judgments when solving problems, thereby improving the overall quality of your work. Here is a simple example. Every programming language has a built-in sorting function:

    • If we have not studied data structures and algorithms, we might simply feed any given data to this sorting function. It runs smoothly with good performance, and there doesn't seem to be any problem.
    • But if we have studied algorithms, we would know that the time complexity of the built-in sorting function is \\(O(n \\log n)\\). However, if the given data consists of integers with a fixed number of digits (such as student IDs), we can use the more efficient \"radix sort\", reducing the time complexity to \\(O(nk)\\), where \\(k\\) is the number of digits. When the data volume is very large, the saved running time can create significant value (reduced costs, improved experience, etc.).

    In engineering, many problems are difficult to solve optimally, and many others are only solved \"well enough.\" The difficulty of a problem depends, on the one hand, on the nature of the problem itself and, on the other hand, on the knowledge of the person examining it. The more complete a person's knowledge and the more experience they have, the deeper their analysis will be, and the more elegantly the problem can be solved.

    ","path":["Chapter 1. Encounter with Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   What Is an Algorithm","text":"","path":["Chapter 1. Encounter with Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121-algorithm-definition","level":2,"title":"1.2.1   Algorithm Definition","text":"

    An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time. It has the following characteristics.

    • The problem is well-defined, with clear input and output definitions.
    • It is feasible and can be completed with finite steps, time, and memory.
    • Each step has a definite meaning, and under the same input and operating conditions, the output is always the same.
    ","path":["Chapter 1. Encounter with Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122-data-structure-definition","level":2,"title":"1.2.2   Data Structure Definition","text":"

    A data structure is a way of organizing and storing data, including the data itself, the relationships between data elements, and the methods used to operate on them. It has the following design objectives.

    • Occupy as little space as possible to save computer memory.
    • Data operations should be as fast as possible, covering data access, addition, deletion, update, etc.
    • Provide a concise data representation and logical information so that algorithms can run efficiently.

    Data structure design is a process full of trade-offs. If we want to achieve improvements in one aspect, we often need to make compromises in another aspect. Here are two examples.

    • Compared to arrays, linked lists are more convenient for data addition and deletion operations but sacrifice data access speed.
    • Compared to linked lists, graphs provide richer logical information but require larger memory space.
    ","path":["Chapter 1. Encounter with Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123-the-relationship-between-data-structures-and-algorithms","level":2,"title":"1.2.3   The Relationship Between Data Structures and Algorithms","text":"

    As shown in Figure 1-4, data structures and algorithms are highly related and tightly coupled, specifically manifested in the following three aspects.

    • Data structures are the foundation of algorithms. Data structures provide algorithms with structured storage of data and methods for operating on data.
    • Algorithms breathe life into data structures. Data structures themselves only store data information; combined with algorithms, they can solve specific problems.
    • Algorithms can usually be implemented based on different data structures, but execution efficiency may vary greatly. Choosing the appropriate data structure is key.

    Figure 1-4   The relationship between data structures and algorithms

    Data structures and algorithms are like assembling building blocks as shown in Figure 1-5. A set of building blocks, in addition to containing many parts, also comes with detailed assembly instructions. By following the instructions step by step, we can assemble an exquisite building block model.

    Figure 1-5   Assembling blocks

    The detailed correspondence between the two is shown in Table 1-1.

    Table 1-1   Comparing data structures and algorithms to assembling building blocks

    Data structures and algorithms Assembling building blocks Input data Unassembled building blocks Data structure Organization form of building blocks, including shape, size, connection method, etc. Algorithm A series of operational steps to assemble the blocks into the target form Output data Building block model

    It is worth noting that data structures and algorithms are independent of programming languages. That is why this book can provide implementations in multiple programming languages.

    Conventional abbreviation

    In actual discussions, we usually abbreviate \"data structures and algorithms\" as \"algorithms\". For example, the well-known LeetCode algorithm problems actually examine knowledge of both data structures and algorithms.

    ","path":["Chapter 1. Encounter with Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"Chapter 0.   Preface","text":"

    Abstract

    Algorithms are like a beautiful symphony, each line of code flows like a melody.

    May this book gently resonate in your mind, leaving a unique and profound melody.

    ","path":["Chapter 0. Preface","Chapter 0.   Preface"],"tags":[]},{"location":"chapter_preface/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 0.1   About This Book
    • 0.2   How to Use This Book
    • 0.3   Summary
    ","path":["Chapter 0. Preface","Chapter 0.   Preface"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   About This Book","text":"

    This project aims to create an open-source, free, beginner-friendly introductory tutorial on data structures and algorithms.

    • The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners through the landscape of data structures and algorithms.
    • The source code can be run with one click, helping readers improve their programming skills through practice and understand how algorithms work and the underlying implementation of data structures.
    • We encourage readers to learn from each other, and everyone is welcome to ask questions and share insights in the comments section, making progress together through discussion and exchange.
    ","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#011-target-audience","level":2,"title":"0.1.1   Target Audience","text":"

    If you are an algorithm beginner who has never studied algorithms, or if you already have some problem-solving experience but only a hazy understanding of data structures and algorithms, then this book is tailor-made for you!

    If you have already accumulated a certain amount of problem-solving experience and are familiar with most question types, this book can help you review and organize your algorithm knowledge system, and the repository's source code can be used as a \"problem-solving toolkit\" or \"algorithm dictionary.\"

    If you are an algorithm \"expert,\" we look forward to receiving your valuable suggestions, or joining us as a contributor.

    Prerequisites

    You need basic programming knowledge in at least one language and the ability to read and write simple code.

    ","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#012-content-structure","level":2,"title":"0.1.2   Content Structure","text":"

    The main content of this book is shown in Figure 0-1.

    • Complexity analysis: Evaluation dimensions and methods for data structures and algorithms. Methods for calculating time complexity and space complexity, common types, examples, etc.
    • Data structures: Classification methods for basic data types and data structures. Definitions, advantages and disadvantages, common operations, common types, typical applications, implementation methods, and more for data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs.
    • Algorithms: The definition, advantages and disadvantages, efficiency, application scenarios, problem-solving steps, and example problems of algorithms such as searching, sorting, divide and conquer, backtracking, dynamic programming, and greedy algorithms.

    Figure 0-1   Main content of this book

    ","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#013-acknowledgements","level":2,"title":"0.1.3   Acknowledgements","text":"

    This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every contributor who invested time and effort, they are (in the order automatically generated by 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, and KeiichiKasai.

    The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in; they helped keep the code consistent and standardized across the different language versions.

    The English version of this book was reviewed by yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn and thomasq0; the Japanese version was reviewed by eltociear; the Russian version was reviewed by И. А. Шевкун and Yuyan Huang; and the Traditional Chinese version was reviewed by Shyam-Chen and Dr-XYZ. Thanks to their contributions, this book is able to serve a broader readership, and we are deeply grateful to them.

    The ePub ebook generation tool for this book was developed by zhongfq. We thank him for his contribution, which provides readers with a more flexible way to read.

    During the creation of this book, I received help from many people.

    • Thanks to my mentor at the company, Dr. Li Xi, who encouraged me to \"take action quickly\" during a conversation, strengthening my determination to write this book;
    • Thanks to my girlfriend Bubble, the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more approachable for beginners;
    • Thanks to Tengbao, Qibao, and Feibao for coming up with a creative name for this book, evoking everyone's fond memories of writing their first line of code \"Hello World!\";
    • Thanks to Xiaoquan for providing professional help in intellectual property rights, which played an important role in the improvement of this open-source book;
    • Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently revising them many times at my perfectionist insistence;
    • Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme Material-for-MkDocs.

    During the writing process, I read many textbooks and articles on data structures and algorithms. These works served as excellent models for this book and helped ensure the accuracy and quality of its content. I would like to thank all the teachers and predecessors for their outstanding contributions!

    This book advocates a hands-on approach to learning, and in this respect I was deeply inspired by Dive into Deep Learning. I highly recommend this excellent work to all readers.

    Heartfelt thanks to my parents. It is your support and encouragement that gave me the opportunity to pursue this enjoyable project.

    ","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   How to Use This Book","text":"

    Tip

    For the best reading experience, it is recommended that you read through this section.

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#021-writing-style-conventions","level":2,"title":"0.2.1   Writing Style Conventions","text":"
    • Sections marked with * after the title are optional and somewhat more challenging. If you're short on time, you can skip them on your first pass.
    • Technical terms are shown in bold (in the print and PDF editions) or underlined (in the web edition), such as array. They are worth remembering, as they will help when reading technical literature.
    • Key content and summary statements will be bolded, and such text deserves special attention.
    • Words and phrases with specific meanings will be marked with \"quotation marks\" to avoid ambiguity.
    • When terminology differs across programming languages, this book follows Python conventions; for example, it uses None to represent \"null\".
    • This book partially relaxes conventional programming-language comment styles in favor of a more compact layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    \"\"\"Title comment, used to label functions, classes, test cases, etc.\"\"\"\n\n# Content comment, used to explain code in detail\n\n\"\"\"\nMulti-line\ncomment\n\"\"\"\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n// Multi-line\n// comment\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    /* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
    ### Title comment, used to label functions, classes, test cases, etc. ###\n\n# Content comment, used to explain code in detail\n\n# Multi-line\n# comment\n
    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#022-learning-efficiently-with-animated-illustrations","level":2,"title":"0.2.2   Learning Efficiently with Animated Illustrations","text":"

    Compared with plain text, videos and images have higher information density and a clearer structure, making them easier to understand. In this book, key concepts and challenging topics are presented mainly through animated illustrations, with text serving as explanation and supplement.

    If, while reading this book, you encounter an animated illustration like the one shown below, treat the illustration as primary and the text as supplementary, and use both together to understand the content.

    Figure 0-2   Example of animated illustrations

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#023-deepening-understanding-through-code-practice","level":2,"title":"0.2.3   Deepening Understanding Through Code Practice","text":"

    The accompanying code for this book is hosted in the GitHub repository. As shown in Figure 0-3, the source code comes with test cases and can be run with one click.

    If time permits, it is recommended that you type out the code yourself. If you have limited study time, please at least read through and run all the code.

    Compared with simply reading code, writing it yourself often brings greater rewards. Hands-on practice is where real learning happens.

    Figure 0-3   Example of running code

    Getting the code running mainly involves three preliminary steps.

    Step 1: Install the local programming environment. Please follow the tutorial in the appendix. If it is already installed, you can skip this step.

    Step 2: Clone or download the code repository. Visit the GitHub repository. If you have already installed Git, you can clone this repository with the following command:

    git clone https://github.com/krahets/hello-algo.git\n

    Alternatively, you can click the \"Download ZIP\" button shown below to download a ZIP archive of the repository directly and then extract it locally.

    Figure 0-4   Clone repository and download code

    Step 3: Run the source code. As shown in Figure 0-5, for code blocks with file names at the top, we can find the corresponding source code files in the codes folder of the repository. The source code files can be run with one click, which will help you save unnecessary debugging time and allow you to focus on learning content.

    Figure 0-5   Code blocks and corresponding source code files

    In addition to running code locally, the web version also supports visual execution of Python code (implemented based on pythontutor). As shown in Figure 0-6, you can click \"Visual Run\" below the code block to expand the view and observe the execution process of the algorithm code; you can also click \"Full Screen View\" for a better viewing experience.

    Figure 0-6   Visual running of Python code

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#024-growing-together-through-questions-and-discussions","level":2,"title":"0.2.4   Growing Together Through Questions and Discussions","text":"

    When reading this book, please do not skip over points that you still do not fully understand. Feel free to ask your questions in the comments section, and my friends and I will do our best to answer them, usually within two days.

    As shown in Figure 0-7, the web version has a comments section at the bottom of each chapter. I encourage you to pay close attention to the discussions there. On the one hand, you can learn about the problems that others encounter, thereby filling gaps in your own understanding and prompting deeper thought. On the other hand, I hope you will generously answer other readers' questions, share your insights, and help others improve.

    Figure 0-7   Example of comments section

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#025-algorithm-learning-roadmap","level":2,"title":"0.2.5   Algorithm Learning Roadmap","text":"

    Overall, we can divide the process of learning data structures and algorithms into three stages.

    1. Stage 1: Algorithm introduction. We need to familiarize ourselves with the characteristics and usage of various data structures, and learn the principles, processes, uses, and efficiency of different algorithms.
    2. Stage 2: Practice algorithm problems. It is recommended to start with popular problems and solve at least 100 of them first, so that you become familiar with mainstream algorithm questions. When you first begin practicing problems, \"knowledge forgetting\" may feel like a challenge, but rest assured, this is very normal. We can review problems according to the \"Ebbinghaus forgetting curve\", and after 3-5 rounds of repetition, they usually stick firmly in memory. For recommended problem lists and practice plans, please see this GitHub repository.
    3. Stage 3: Building a knowledge system. In terms of learning, we can read algorithm column articles, problem-solving frameworks, and algorithm textbooks to continuously enrich our knowledge system. In terms of practicing problems, we can try advanced problem-solving strategies, such as categorization by topic, one problem multiple solutions, one solution multiple problems, etc. Related problem-solving insights can be found in various communities.

    As shown in Figure 0-8, the content of this book mainly covers \"Stage 1\", aiming to help you more efficiently carry out Stage 2 and Stage 3 learning.

    Figure 0-8   Algorithm learning roadmap

    ","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   Summary","text":"","path":["Chapter 0. Preface","0.3   Summary"],"tags":[]},{"location":"chapter_preface/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • The main audience of this book is algorithm beginners. If you already have some background, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a \"problem-solving toolkit.\"
    • The content of the book mainly includes three parts: complexity analysis, data structures, and algorithms, covering most topics in this field.
    • For algorithm novices, reading an introductory book during the initial learning stage is crucial, as it can help you avoid many detours.
    • The animated illustrations in the book are usually used to introduce key concepts and challenging topics. When reading this book, you should pay more attention to these topics.
    • Practice is the best way to learn programming. It is strongly recommended to run the source code and type the code yourself.
    • The web version of this book has a comments section for each chapter, where you are welcome to share your questions and insights at any time.
    ","path":["Chapter 0. Preface","0.3   Summary"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"References","text":"

    [1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).

    [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

    [3] Robert Sedgewick, et al. Algorithms (4th Edition).

    [4] Yan Weimin. Data Structures (C Language Version).

    [5] Deng Junhui. Data Structures (C++ Language Version, Third Edition).

    [6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition).

    [7] Cheng Jie. Data Structures in Plain Language.

    [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, et al. Dive into Deep Learning.

    ","path":["References"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"Chapter 10.   Searching","text":"

    Abstract

    Searching is an adventure into the unknown, where we may need to traverse every corner of the mysterious space, or we may be able to quickly lock onto the target.

    In this journey of discovery, each exploration may yield an unexpected answer.

    ","path":["Chapter 10. Searching","Chapter 10.   Searching"],"tags":[]},{"location":"chapter_searching/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 10.1   Binary Search
    • 10.2   Binary Search Insertion Point
    • 10.3   Binary Search Boundaries
    • 10.4   Hash Optimization Strategy
    • 10.5   Searching Algorithms Revisited
    • 10.6   Summary
    ","path":["Chapter 10. Searching","Chapter 10.   Searching"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   Binary Search","text":"

    Binary search is an efficient search algorithm based on the divide-and-conquer strategy. It leverages the sorted order of the data to reduce the search range by half in each round until the target element is found or the search interval becomes empty.

    Question

    Given an array nums of length \\(n\\) with elements arranged in ascending order and no duplicates, search for and return the index of element target in the array. If the array does not contain the element, return \\(-1\\). An example is shown in Figure 10-1.

    Figure 10-1   Binary search example data

    As shown in Figure 10-2, we first initialize pointers \\(i = 0\\) and \\(j = n - 1\\), pointing to the first and last elements of the array respectively, representing the search interval \\([0, n - 1]\\). Note that square brackets denote a closed interval, which includes the boundary values themselves.

    Next, perform the following two steps in a loop:

    1. Calculate the midpoint index \\(m = \\lfloor {(i + j) / 2} \\rfloor\\), where \\(\\lfloor \\: \\rfloor\\) denotes the floor operation.
    2. Compare nums[m] and target, which results in three cases:
      1. When nums[m] < target, it indicates that target is in the interval \\([m + 1, j]\\), so execute \\(i = m + 1\\).
      2. When nums[m] > target, it indicates that target is in the interval \\([i, m - 1]\\), so execute \\(j = m - 1\\).
      3. When nums[m] = target, it indicates that target has been found, so return index \\(m\\).

    If the array does not contain the target element, the search interval will eventually become empty. In this case, return \\(-1\\).

    <1><2><3><4><5><6><7>

    Figure 10-2   Binary search process

    It's worth noting that since both \\(i\\) and \\(j\\) are of int type, \\(i + j\\) may exceed the range of the int type. To avoid integer overflow, we typically use the formula \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) to calculate the midpoint.

    The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
    def binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Binary search (closed interval)\"\"\"\n    # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    i, j = 0, len(nums) - 1\n    # Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j:\n        # In theory, Python numbers can be infinitely large (depending on memory size), no need to consider large number overflow\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # This means target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # This means target is in the interval [i, m-1]\n        else:\n            return m  # Found the target element, return its index\n    return -1  # Target element not found, return -1\n
    binary_search.cpp
    /* Binary search (closed interval on both sides) */\nint binarySearch(vector<int> &nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.size() - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.java
    /* Binary search (closed interval on both sides) */\nint binarySearch(int[] nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.cs
    /* Binary search (closed interval on both sides) */\nint BinarySearch(int[] nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.Length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2;   // Calculate the midpoint index m\n        if (nums[m] < target)      // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else                       // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.go
    /* Binary search (closed interval on both sides) */\nfunc binarySearch(nums []int, target int) int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    i, j := 0, len(nums)-1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    for i <= j {\n        m := i + (j-i)/2      // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m-1]\n            j = m - 1\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.swift
    /* Binary search (closed interval on both sides) */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m-1]\n            j = m - 1\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.js
    /* Binary search (closed interval on both sides) */\nfunction binarySearch(nums, target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let i = 0,\n        j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        // Calculate midpoint index m, use parseInt() to round down\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target)\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else return m; // Found the target element, return its index\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.ts
    /* Binary search (closed interval on both sides) */\nfunction binarySearch(nums: number[], target: number): number {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let i = 0,\n        j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        // Calculate the midpoint index m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    return -1; // Target element not found, return -1\n}\n
    binary_search.dart
    /* Binary search (closed interval on both sides) */\nint binarySearch(List<int> nums, int target) {\n  // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n  int i = 0, j = nums.length - 1;\n  // Loop, exit when the search interval is empty (empty when i > j)\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      // This means target is in the interval [m+1, j]\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // This means target is in the interval [i, m-1]\n      j = m - 1;\n    } else {\n      // Found the target element, return its index\n      return m;\n    }\n  }\n  // Target element not found, return -1\n  return -1;\n}\n
    binary_search.rs
    /* Binary search (closed interval on both sides) */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let mut i = 0;\n    let mut j = nums.len() as i32 - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.c
    /* Binary search (closed interval on both sides) */\nint binarySearch(int *nums, int len, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = len - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.kt
    /* Binary search (closed interval on both sides) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    var i = 0\n    var j = nums.size - 1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j]\n            i = m + 1\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1\n        else  // Found the target element, return its index\n            return m\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.rb
    ### Binary search (closed interval) ###\ndef binary_search(nums, target)\n  # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n  i, j = 0, nums.length - 1\n\n  # Loop, exit when the search interval is empty (empty when i > j)\n  while i <= j\n    # In theory, Ruby numbers can be infinitely large (limited by memory), no need to consider overflow\n    m = (i + j) / 2   # Calculate the midpoint index m\n\n    if nums[m] < target\n      i = m + 1 # This means target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # This means target is in the interval [i, m-1]\n    else\n      return m  # Found the target element, return its index\n    end\n  end\n\n  -1  # Target element not found, return -1\nend\n

    Time complexity is \\(O(\\log n)\\): In the binary search loop, the interval is reduced by half each round, so the number of iterations is \\(\\log_2 n\\).

    Space complexity is \\(O(1)\\): Pointers \\(i\\) and \\(j\\) use constant-size space.

    ","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search/#1011-interval-representation-methods","level":2,"title":"10.1.1   Interval Representation Methods","text":"

    In addition to the closed interval mentioned above, another common interval representation is the \"left-closed right-open\" interval, defined as \\([0, n)\\), meaning that the left boundary is inclusive while the right boundary is exclusive. Under this representation, the interval \\([i, j)\\) is empty when \\(i = j\\).

    We can implement a binary search algorithm with the same functionality based on this representation:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
    def binary_search_lcro(nums: list[int], target: int) -> int:\n    \"\"\"Binary search (left-closed right-open interval)\"\"\"\n    # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    i, j = 0, len(nums)\n    # Loop, exit when the search interval is empty (empty when i = j)\n    while i < j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # This means target is in the interval [m+1, j)\n        elif nums[m] > target:\n            j = m  # This means target is in the interval [i, m)\n        else:\n            return m  # Found the target element, return its index\n    return -1  # Target element not found, return -1\n
    binary_search.cpp
    /* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.size();\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.java
    /* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(int[] nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.cs
    /* Binary search (left-closed right-open interval) */\nint BinarySearchLCRO(int[] nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.Length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2;   // Calculate the midpoint index m\n        if (nums[m] < target)      // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else                       // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.go
    /* Binary search (left-closed right-open interval) */\nfunc binarySearchLCRO(nums []int, target int) int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    i, j := 0, len(nums)\n    // Loop, exit when the search interval is empty (empty when i = j)\n    for i < j {\n        m := i + (j-i)/2      // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m)\n            j = m\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.swift
    /* Binary search (left-closed right-open interval) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -> Int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    var i = nums.startIndex\n    var j = nums.endIndex\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while i < j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m)\n            j = m\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.js
    /* Binary search (left-closed right-open interval) */\nfunction binarySearchLCRO(nums, target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let i = 0,\n        j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        // Calculate midpoint index m, use parseInt() to round down\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target)\n            // This means target is in the interval [i, m)\n            j = m;\n        // Found the target element, return its index\n        else return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.ts
    /* Binary search (left-closed right-open interval) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let i = 0,\n        j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        // Calculate the midpoint index m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // This means target is in the interval [i, m)\n            j = m;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    return -1; // Target element not found, return -1\n}\n
    binary_search.dart
    /* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(List<int> nums, int target) {\n  // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n  int i = 0, j = nums.length;\n  // Loop, exit when the search interval is empty (empty when i = j)\n  while (i < j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      // This means target is in the interval [m+1, j)\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // This means target is in the interval [i, m)\n      j = m;\n    } else {\n      // Found the target element, return its index\n      return m;\n    }\n  }\n  // Target element not found, return -1\n  return -1;\n}\n
    binary_search.rs
    /* Binary search (left-closed right-open interval) */\nfn binary_search_lcro(nums: &[i32], target: i32) -> i32 {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let mut i = 0;\n    let mut j = nums.len() as i32;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while i < j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // This means target is in the interval [i, m)\n            j = m;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.c
    /* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(int *nums, int len, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = len;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
    binary_search.kt
    /* Binary search (left-closed right-open interval) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    var i = 0\n    var j = nums.size\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j)\n            i = m + 1\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m\n        else  // Found the target element, return its index\n            return m\n    }\n    // Target element not found, return -1\n    return -1\n}\n
    binary_search.rb
    ### Binary search (left-closed right-open interval) ###\ndef binary_search_lcro(nums, target)\n  # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n  i, j = 0, nums.length\n\n  # Loop, exit when the search interval is empty (empty when i = j)\n  while i < j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # This means target is in the interval [m+1, j)\n    elsif nums[m] > target\n      j = m - 1 # This means target is in the interval [i, m)\n    else\n      return m  # Found the target element, return its index\n    end\n  end\n\n  -1  # Target element not found, return -1\nend\n

    As shown in Figure 10-3, under the two interval representations, the initialization, loop condition, and interval narrowing operations of the binary search algorithm are all different.

    Since both the left and right boundaries in the \"closed interval\" representation are defined as closed, the operations to narrow the interval through pointers \\(i\\) and \\(j\\) are also symmetric. This makes it less error-prone, so the \"closed interval\" approach is generally recommended.

    Figure 10-3   Two interval definitions

    ","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search/#1012-advantages-and-limitations","level":2,"title":"10.1.2   Advantages and Limitations","text":"

    Binary search offers good performance in both time and space.

    • Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size \\(n = 2^{20}\\), linear search requires \\(2^{20} = 1048576\\) iterations, while binary search only needs \\(\\log_2 2^{20} = 20\\) iterations.
    • Binary search requires no extra space. Compared to searching algorithms that require additional space (such as hash-based search), binary search is more space-efficient.

    However, binary search is not suitable for all situations, mainly for the following reasons:

    • Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of \\(O(n \\log n)\\), which is higher than both linear search and binary search. For scenarios with frequent element insertions, keeping the array sorted requires inserting elements at specific positions with a time complexity of \\(O(n)\\), which is also very expensive.
    • Binary search is only applicable to arrays. Binary search requires non-contiguous, jump-style access to elements, and this kind of access is inefficient in linked lists, making it unsuitable for linked lists or linked-list-based data structures.
    • For small data volumes, linear search performs better. In linear search, each round requires only 1 comparison operation; while in binary search, it requires 1 addition, 1 division, 1-3 comparison operations, and 1 addition (subtraction), totaling 4-6 unit operations. Therefore, when the data volume \\(n\\) is small, linear search is actually faster than binary search.
    ","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   Binary Search Boundaries","text":"","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031-finding-the-left-boundary","level":2,"title":"10.3.1   Finding the Left Boundary","text":"

    Question

    Given a sorted array nums of length \\(n\\) that may contain duplicate elements, return the index of the leftmost occurrence of target. If the array does not contain target, return \\(-1\\).

    Recall the method for finding the insertion point with binary search. After the search completes, \\(i\\) points to the leftmost target, so finding the insertion point is essentially finding the index of the leftmost target.

    Consider implementing the left boundary search using the insertion point finding function. Note that the array may not contain target, which could result in the following two cases:

    • The insertion point index \\(i\\) is out of bounds.
    • The element nums[i] is not equal to target.

    When either of these situations occurs, simply return \\(-1\\). The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
    def binary_search_left_edge(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for the leftmost target\"\"\"\n    # Equivalent to finding the insertion point of target\n    i = binary_search_insertion(nums, target)\n    # Target not found, return -1\n    if i == len(nums) or nums[i] != target:\n        return -1\n    # Found target, return index i\n    return i\n
    binary_search_edge.cpp
    /* Binary search for the leftmost target */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.size() || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.java
    /* Binary search for the leftmost target */\nint binarySearchLeftEdge(int[] nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binary_search_insertion.binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.length || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.cs
    /* Binary search for the leftmost target */\nint BinarySearchLeftEdge(int[] nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.Length || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.go
    /* Binary search for the leftmost target */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n    // Equivalent to finding the insertion point of target\n    i := binarySearchInsertion(nums, target)\n    // Target not found, return -1\n    if i == len(nums) || nums[i] != target {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
    binary_search_edge.swift
    /* Binary search for the leftmost target */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -> Int {\n    // Equivalent to finding the insertion point of target\n    let i = binarySearchInsertion(nums: nums, target: target)\n    // Target not found, return -1\n    if i == nums.endIndex || nums[i] != target {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
    binary_search_edge.js
    /* Binary search for the leftmost target */\nfunction binarySearchLeftEdge(nums, target) {\n    // Equivalent to finding the insertion point of target\n    const i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.ts
    /* Binary search for the leftmost target */\nfunction binarySearchLeftEdge(nums: Array<number>, target: number): number {\n    // Equivalent to finding the insertion point of target\n    const i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.dart
    /* Binary search for the leftmost target */\nint binarySearchLeftEdge(List<int> nums, int target) {\n  // Equivalent to finding the insertion point of target\n  int i = binarySearchInsertion(nums, target);\n  // Target not found, return -1\n  if (i == nums.length || nums[i] != target) {\n    return -1;\n  }\n  // Found target, return index i\n  return i;\n}\n
    binary_search_edge.rs
    /* Binary search for the leftmost target */\nfn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {\n    // Equivalent to finding the insertion point of target\n    let i = binary_search_insertion(nums, target);\n    // Target not found, return -1\n    if i == nums.len() as i32 || nums[i as usize] != target {\n        return -1;\n    }\n    // Found target, return index i\n    i\n}\n
    binary_search_edge.c
    /* Binary search for the leftmost target */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binarySearchInsertion(nums, numSize, target);\n    // Target not found, return -1\n    if (i == numSize || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
    binary_search_edge.kt
    /* Binary search for the leftmost target */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n    // Equivalent to finding the insertion point of target\n    val i = binarySearchInsertion(nums, target)\n    // Target not found, return -1\n    if (i == nums.size || nums[i] != target) {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
    binary_search_edge.rb
    ### Binary search leftmost target ###\ndef binary_search_left_edge(nums, target)\n  # Equivalent to finding the insertion point of target\n  i = binary_search_insertion(nums, target)\n\n  # Target not found, return -1\n  return -1 if i == nums.length || nums[i] != target\n\n  i # Found target, return index i\nend\n
    ","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032-finding-the-right-boundary","level":2,"title":"10.3.2   Finding the Right Boundary","text":"

    So how do we find the rightmost target? The most direct approach is to modify the code and replace the pointer shrinking operation in the nums[m] == target case. The code is omitted here; interested readers can implement it themselves.

    Below we introduce two more clever methods.

    ","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1-reusing-left-boundary-search","level":3,"title":"1.   Reusing Left Boundary Search","text":"

    In fact, we can use the function for finding the leftmost target to find the rightmost target. The specific method is: convert finding the rightmost target into finding the leftmost target + 1.

    As shown in Figure 10-7, after the search completes, the pointer \\(i\\) points to the leftmost target + 1 (if it exists), while \\(j\\) points to the rightmost target, so we can return \\(j\\).

    Figure 10-7   Converting right boundary search to left boundary search

    Note that the returned insertion point is \\(i\\), so we need to subtract \\(1\\) from it to obtain \\(j\\):

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
    def binary_search_right_edge(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for the rightmost target\"\"\"\n    # Convert to finding the leftmost target + 1\n    i = binary_search_insertion(nums, target + 1)\n    # j points to the rightmost target, i points to the first element greater than target\n    j = i - 1\n    # Target not found, return -1\n    if j == -1 or nums[j] != target:\n        return -1\n    # Found target, return index j\n    return j\n
    binary_search_edge.cpp
    /* Binary search for the rightmost target */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.java
    /* Binary search for the rightmost target */\nint binarySearchRightEdge(int[] nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.cs
    /* Binary search for the rightmost target */\nint BinarySearchRightEdge(int[] nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.go
    /* Binary search for the rightmost target */\nfunc binarySearchRightEdge(nums []int, target int) int {\n    // Convert to finding the leftmost target + 1\n    i := binarySearchInsertion(nums, target+1)\n    // j points to the rightmost target, i points to the first element greater than target\n    j := i - 1\n    // Target not found, return -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
    binary_search_edge.swift
    /* Binary search for the rightmost target */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -> Int {\n    // Convert to finding the leftmost target + 1\n    let i = binarySearchInsertion(nums: nums, target: target + 1)\n    // j points to the rightmost target, i points to the first element greater than target\n    let j = i - 1\n    // Target not found, return -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
    binary_search_edge.js
    /* Binary search for the rightmost target */\nfunction binarySearchRightEdge(nums, target) {\n    // Convert to finding the leftmost target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    const j = i - 1;\n    // Target not found, return -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.ts
    /* Binary search for the rightmost target */\nfunction binarySearchRightEdge(nums: Array<number>, target: number): number {\n    // Convert to finding the leftmost target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    const j = i - 1;\n    // Target not found, return -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.dart
    /* Binary search for the rightmost target */\nint binarySearchRightEdge(List<int> nums, int target) {\n  // Convert to finding the leftmost target + 1\n  int i = binarySearchInsertion(nums, target + 1);\n  // j points to the rightmost target, i points to the first element greater than target\n  int j = i - 1;\n  // Target not found, return -1\n  if (j == -1 || nums[j] != target) {\n    return -1;\n  }\n  // Found target, return index j\n  return j;\n}\n
    binary_search_edge.rs
    /* Binary search for the rightmost target */\nfn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {\n    // Convert to finding the leftmost target + 1\n    let i = binary_search_insertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    let j = i - 1;\n    // Target not found, return -1\n    if j == -1 || nums[j as usize] != target {\n        return -1;\n    }\n    // Found target, return index j\n    j\n}\n
    binary_search_edge.c
    /* Binary search for the rightmost target */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binarySearchInsertion(nums, numSize, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
    binary_search_edge.kt
    /* Binary search for the rightmost target */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n    // Convert to finding the leftmost target + 1\n    val i = binarySearchInsertion(nums, target + 1)\n    // j points to the rightmost target, i points to the first element greater than target\n    val j = i - 1\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
    binary_search_edge.rb
    ### Binary search rightmost target ###\ndef binary_search_right_edge(nums, target)\n  # Convert to finding the leftmost target + 1\n  i = binary_search_insertion(nums, target + 1)\n\n  # j points to the rightmost target, i points to the first element greater than target\n  j = i - 1\n\n  # Target not found, return -1\n  return -1 if j == -1 || nums[j] != target\n\n  j # Found target, return index j\nend\n
    ","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2-converting-to-element-search","level":3,"title":"2.   Converting to Element Search","text":"

    We know that when the array does not contain target, \\(i\\) and \\(j\\) will eventually point to the first elements greater than and less than target, respectively.

    Therefore, as shown in Figure 10-8, we can construct an element that does not exist in the array to find the left and right boundaries.

    • Finding the leftmost target: This can be converted to finding target - 0.5 and returning the pointer \\(i\\).
    • Finding the rightmost target: This can be converted to finding target + 0.5 and returning the pointer \\(j\\).

    Figure 10-8   Converting boundary search to element search

    The code is omitted here, but the following two points are worth noting:

    • Since the given array does not contain decimal values, we do not need to worry about how to handle equality.
    • Because this method introduces decimals, the variable target in the function needs to be changed to a floating-point type (Python does not require this change).
    ","path":["Chapter 10. Searching","10.3   Binary Search Boundaries"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   Binary Search Insertion Point","text":"

    Binary search can be used not only to search for target elements, but also to solve many variant problems, such as finding the insertion position of a target element.

    ","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021-case-without-duplicate-elements","level":2,"title":"10.2.1   Case Without Duplicate Elements","text":"

    Question

    Given a sorted array nums of length \\(n\\) and an element target, where the array contains no duplicate elements, insert target into nums while maintaining its sorted order. If target already exists in the array, insert it to its left. Return the index of target after insertion. An example is shown below.

    Figure 10-4   Binary search insertion point example data

    If we want to reuse the binary search code from the previous section, we need to answer the following two questions.

    Question 1: When the array contains target, is the insertion point index the same as that element's index?

    The problem requires inserting target to the left of equal elements, which means the newly inserted target replaces the position of the original target. In other words, when the array contains target, the insertion point index is the index of that target.

    Question 2: When the array does not contain target, what is the insertion point index?

    To analyze this further, consider the binary search process: when nums[m] < target, \\(i\\) moves, meaning that pointer \\(i\\) is approaching elements greater than or equal to target. Similarly, pointer \\(j\\) is always approaching elements less than or equal to target.

    Therefore, when the binary search ends, \\(i\\) must point to the first element greater than target, and \\(j\\) must point to the first element less than target. It follows that when the array does not contain target, the insertion index is \\(i\\). The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
    def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for insertion point (no duplicate elements)\"\"\"\n    i, j = 0, len(nums) - 1  # Initialize closed interval [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target is in the interval [i, m-1]\n        else:\n            return m  # Found target, return insertion point m\n    # Target not found, return insertion point i\n    return i\n
    binary_search_insertion.cpp
    /* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.java
    /* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.cs
    /* Binary search for insertion point (no duplicate elements) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.go
    /* Binary search for insertion point (no duplicate elements) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n    // Initialize closed interval [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Calculate the midpoint index m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target is in the interval [i, m-1]\n            j = m - 1\n        } else {\n            // Found target, return insertion point m\n            return m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
    binary_search_insertion.swift
    /* Binary search for insertion point (no duplicate elements) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [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 // Calculate the midpoint index m\n        if nums[m] < target {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            return m // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
    binary_search_insertion.js
    /* Binary search for insertion point (no duplicate elements) */\nfunction binarySearchInsertionSimple(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.ts
    /* Binary search for insertion point (no duplicate elements) */\nfunction binarySearchInsertionSimple(\n    nums: Array<number>,\n    target: number\n): number {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.dart
    /* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      i = m + 1; // target is in the interval [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target is in the interval [i, m-1]\n    } else {\n      return m; // Found target, return insertion point m\n    }\n  }\n  // Target not found, return insertion point i\n  return i;\n}\n
    binary_search_insertion.rs
    /* Binary search for insertion point (no duplicate elements) */\nfn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m;\n        }\n    }\n    // Target not found, return insertion point i\n    i\n}\n
    binary_search_insertion.c
    /* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
    binary_search_insertion.kt
    /* Binary search for insertion point (no duplicate elements) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            return m // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
    binary_search_insertion.rb
    ### Binary search insertion point (no duplicates) ###\ndef binary_search_insertion_simple(nums, target)\n  # Initialize closed interval [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target is in the interval [i, m-1]\n    else\n      return m  # Found target, return insertion point m\n    end\n  end\n\n  i # Target not found, return insertion point i\nend\n
    ","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022-case-with-duplicate-elements","level":2,"title":"10.2.2   Case with Duplicate Elements","text":"

    Question

    Based on the previous problem, assume the array may contain duplicate elements, with everything else remaining the same.

    Suppose there are multiple target elements in the array. Ordinary binary search can only return the index of one target, and cannot determine how many target elements are to the left and right of that element.

    The problem requires inserting the target element at the leftmost position, so we need to find the index of the leftmost target in the array. A straightforward initial approach is to follow the steps shown in Figure 10-5:

    1. Perform binary search to obtain the index of any target, denoted as \\(k\\).
    2. Starting from index \\(k\\), perform linear traversal to the left, and return when the leftmost target is found.

    Figure 10-5   Linear search for insertion point of duplicate elements

    Although this method works, it includes linear search, resulting in a time complexity of \\(O(n)\\). When the array contains many duplicate target elements, this method is very inefficient.

    Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains unchanged: in each iteration, we first compute the midpoint index \\(m\\), then compare target with nums[m], leading to the following cases:

    • When nums[m] < target or nums[m] > target, it means target has not been found yet, so use the standard interval-shrinking operation of binary search to move pointers \\(i\\) and \\(j\\) closer to target.
    • When nums[m] == target, it means elements less than target are in the interval \\([i, m - 1]\\), so use \\(j = m - 1\\) to shrink the interval, thereby moving pointer \\(j\\) closer to elements less than target.

    After the loop completes, \\(i\\) points to the leftmost target, and \\(j\\) points to the first element less than target, so index \\(i\\) is the insertion point.

    <1><2><3><4><5><6><7><8>

    Figure 10-6   Steps for binary search insertion point of duplicate elements

    Observe the following code: the branches nums[m] > target and nums[m] == target perform the same operation, so they can be merged.

    Even so, we can still keep the conditional branches expanded, as the logic is clearer and more readable.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
    def binary_search_insertion(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for insertion point (with duplicate elements)\"\"\"\n    i, j = 0, len(nums) - 1  # Initialize closed interval [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target is in the interval [i, m-1]\n        else:\n            j = m - 1  # The first element less than target is in the interval [i, m-1]\n    # Return insertion point i\n    return i\n
    binary_search_insertion.cpp
    /* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.java
    /* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.cs
    /* Binary search for insertion point (with duplicate elements) */\nint BinarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.go
    /* Binary search for insertion point (with duplicate elements) */\nfunc binarySearchInsertion(nums []int, target int) int {\n    // Initialize closed interval [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Calculate the midpoint index m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target is in the interval [i, m-1]\n            j = m - 1\n        } else {\n            // The first element less than target is in the interval [i, m-1]\n            j = m - 1\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
    binary_search_insertion.swift
    /* Binary search for insertion point (with duplicate elements) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [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 // Calculate the midpoint index m\n        if nums[m] < target {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            j = m - 1 // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
    binary_search_insertion.js
    /* Binary search for insertion point (with duplicate elements) */\nfunction binarySearchInsertion(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.ts
    /* Binary search for insertion point (with duplicate elements) */\nfunction binarySearchInsertion(nums: Array<number>, target: number): number {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.dart
    /* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      i = m + 1; // target is in the interval [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target is in the interval [i, m-1]\n    } else {\n      j = m - 1; // The first element less than target is in the interval [i, m-1]\n    }\n  }\n  // Return insertion point i\n  return i;\n}\n
    binary_search_insertion.rs
    /* Binary search for insertion point (with duplicate elements) */\npub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    i\n}\n
    binary_search_insertion.c
    /* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
    binary_search_insertion.kt
    /* Binary search for insertion point (with duplicate elements) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            j = m - 1 // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
    binary_search_insertion.rb
    ### Binary search insertion point (with duplicates) ###\ndef binary_search_insertion(nums, target)\n  # Initialize closed interval [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target is in the interval [i, m-1]\n    else\n      j = m - 1 # The first element less than target is in the interval [i, m-1]\n    end\n  end\n\n  i # Return insertion point i\nend\n

    Tip

    The code in this section uses the \"closed interval\" approach throughout. Interested readers can implement the \"left-closed, right-open\" approach themselves.

    Overall, binary search is simply a matter of setting separate search targets for pointers \\(i\\) and \\(j\\). The target may be a specific element (such as target) or a range of elements (such as elements less than target).

    With each iteration of binary search, pointers \\(i\\) and \\(j\\) gradually approach their preset targets. Ultimately, they either find the answer or stop after crossing the boundary.

    ","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   Hash Optimization Strategy","text":"

    In algorithm problems, we often reduce the time complexity of algorithms by replacing linear search with hash-based search. Let's use an algorithm problem to deepen our understanding.

    Question

    Given an integer array nums and a target value target, find two elements in the array whose sum is target, and return their indices. Any solution will do.

    ","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041-linear-search-trading-time-for-space","level":2,"title":"10.4.1   Linear Search: Trading Time for Space","text":"

    Consider directly traversing all possible combinations. As shown in Figure 10-9, we use nested loops and check in each iteration whether the sum of two integers is target. If so, return their indices.

    Figure 10-9   Linear search solution for two sum

    The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
    def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"Method 1: Brute force enumeration\"\"\"\n    # Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nint[] twoSumBruteForce(int[] nums, int target) {\n    int size = nums.length;\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nint[] TwoSumBruteForce(int[] nums, int target) {\n    int size = nums.Length;\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nfunc twoSumBruteForce(nums []int, target int) []int {\n    size := len(nums)\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nfunc twoSumBruteForce(nums: [Int], target: Int) -> [Int] {\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nfunction twoSumBruteForce(nums, target) {\n    const n = nums.length;\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n    const n = nums.length;\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\nList<int> twoSumBruteForce(List<int> nums, int target) {\n  int size = nums.length;\n  // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\npub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    let size = nums.len();\n    // Two nested loops, time complexity is 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
    /* Method 1: Brute force enumeration */\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
    /* Method 1: Brute force enumeration */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Two nested loops, time complexity is 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
    ### Method 1: Brute force enumeration ###\ndef two_sum_brute_force(nums, target)\n  # Two nested loops, time complexity is 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

    This method has a time complexity of \\(O(n^2)\\) and a space complexity of \\(O(1)\\), making it very time-consuming on large inputs.

    ","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042-hash-based-search-trading-space-for-time","level":2,"title":"10.4.2   Hash-Based Search: Trading Space for Time","text":"

    Consider using a hash table whose keys are array elements and whose values are their indices. Traverse the array and perform the steps shown in Figure 10-10 in each iteration:

    1. Check if the number target - nums[i] is in the hash table. If so, directly return the indices of these two elements.
    2. Add the key-value pair nums[i] and index i to the hash table.
    <1><2><3>

    Figure 10-10   Hash table solution for two sum

    The implementation is shown below and requires only a single loop:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
    def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n    \"\"\"Method 2: Auxiliary hash table\"\"\"\n    # Auxiliary hash table, space complexity is O(n)\n    dic = {}\n    # Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Auxiliary hash table, space complexity is O(n)\n    unordered_map<int, int> dic;\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nint[] twoSumHashTable(int[] nums, int target) {\n    int size = nums.length;\n    // Auxiliary hash table, space complexity is O(n)\n    Map<Integer, Integer> dic = new HashMap<>();\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nint[] TwoSumHashTable(int[] nums, int target) {\n    int size = nums.Length;\n    // Auxiliary hash table, space complexity is O(n)\n    Dictionary<int, int> dic = [];\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nfunc twoSumHashTable(nums []int, target int) []int {\n    // Auxiliary hash table, space complexity is O(n)\n    hashTable := map[int]int{}\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nfunc twoSumHashTable(nums: [Int], target: Int) -> [Int] {\n    // Auxiliary hash table, space complexity is O(n)\n    var dic: [Int: Int] = [:]\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nfunction twoSumHashTable(nums, target) {\n    // Auxiliary hash table, space complexity is O(n)\n    let m = {};\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n    // Auxiliary hash table, space complexity is O(n)\n    let m: Map<number, number> = new Map();\n    // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\nList<int> twoSumHashTable(List<int> nums, int target) {\n  int size = nums.length;\n  // Auxiliary hash table, space complexity is O(n)\n  Map<int, int> dic = HashMap();\n  // Single loop, time complexity is 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
    /* Method 2: Auxiliary hash table */\npub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    // Auxiliary hash table, space complexity is O(n)\n    let mut dic = HashMap::new();\n    // Single loop, time complexity is 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
    /* Hash table */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Implemented using uthash.h\n} HashTable;\n\n/* Hash table lookup */\nHashTable *find(HashTable *h, int key) {\n    HashTable *tmp;\n    HASH_FIND_INT(h, &key, tmp);\n    return tmp;\n}\n\n/* Hash table element insertion */\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/* Method 2: Auxiliary hash table */\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
    /* Method 2: Auxiliary hash table */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Auxiliary hash table, space complexity is O(n)\n    val dic = HashMap<Int, Int>()\n    // Single loop, time complexity is 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
    ### Method 2: Auxiliary hash table ###\ndef two_sum_hash_table(nums, target)\n  # Auxiliary hash table, space complexity is O(n)\n  dic = {}\n  # Single loop, time complexity is 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

    This method reduces the time complexity from \\(O(n^2)\\) to \\(O(n)\\) through hash-based search, greatly improving runtime efficiency.

    Since an additional hash table needs to be maintained, the space complexity is \\(O(n)\\). Nevertheless, this method offers a more balanced overall time-space trade-off, making it the optimal solution to this problem.

    ","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   Searching Algorithms Revisited","text":"

    Searching algorithms are used to search for one or a group of elements that meet specific conditions in data structures (such as arrays, linked lists, trees, or graphs).

    Searching algorithms can be divided into the following two categories based on their implementation approach:

    • Locating target elements by traversing the data structure, such as traversing arrays, linked lists, trees, and graphs.
    • Achieving efficient element lookup by leveraging the way data is organized or prior information about the data, such as binary search, hash-based search, and binary search tree search.

    As these topics have already been introduced in earlier chapters, searching algorithms should already be familiar to us. In this section, we revisit them from a more systematic perspective.

    ","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051-brute-force-search","level":2,"title":"10.5.1   Brute-Force Search","text":"

    Brute-force search locates target elements by traversing each element of the data structure.

    • \"Linear search\" is applicable to linear data structures such as arrays and linked lists. It starts from one end of the data structure and accesses elements one by one until the target element is found or the other end is reached without finding the target element.
    • \"Breadth-first search\" and \"depth-first search\" are two traversal strategies for graphs and trees. Breadth-first search starts from the initial node and searches layer by layer, visiting nodes from near to far. Depth-first search starts from the initial node, follows a path to the end, then backtracks and tries other paths until the entire data structure is traversed.

    The advantage of brute-force search is that it is simple and has good generality, requiring no data preprocessing or additional data structures.

    However, the time complexity of such algorithms is \\(O(n)\\), where \\(n\\) is the number of elements, so performance is poor when dealing with large amounts of data.

    ","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052-adaptive-search","level":2,"title":"10.5.2   Adaptive Search","text":"

    Adaptive search leverages properties of the data itself (such as sorted order) to optimize the search process and locate target elements more efficiently.

    • \"Binary search\" uses the orderliness of data to achieve efficient searching, applicable only to arrays.
    • \"Hash-based search\" uses hash tables to store searchable data as key-value pairs, thereby enabling efficient queries.
    • \"Tree search\" operates on specific tree structures (such as binary search trees), quickly ruling out nodes by comparing node values to locate the target element.

    The advantage of such algorithms is high efficiency, with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\).

    However, using these algorithms often requires data preprocessing. For example, binary search requires pre-sorting the array, while hash-based search and tree search both require additional data structures, and maintaining these data structures also requires extra time and space overhead.

    Tip

    Adaptive search algorithms are often called lookup algorithms, mainly used to quickly retrieve target elements in specific data structures.

    ","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053-search-method-selection","level":2,"title":"10.5.3   Search Method Selection","text":"

    Given a dataset of size \\(n\\), we can use linear search, binary search, tree search, hash-based search, and other methods to search for the target element. The working principles of each method are shown in Figure 10-11.

    Figure 10-11   Multiple search strategies

    The efficiency and characteristics of these methods are summarized in Table 10-1.

    Table 10-1   Comparison of search algorithm efficiency

    Linear search Binary search Tree search Hash-based search Search element \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) Insert element \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Delete element \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Extra space \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) Data preprocessing / Sorting \\(O(n \\log n)\\) Tree building \\(O(n \\log n)\\) Hash table building \\(O(n)\\) Data ordered Unordered Ordered Ordered Unordered

    The choice of search algorithm also depends on data volume, search performance requirements, data query and update frequency, etc.

    Linear search

    • Good generality, requiring no data preprocessing operations. If we need to query the data only once, the preprocessing required by the other three methods can take longer than the linear search itself.
    • Suitable for small data volumes, where time complexity has less impact on efficiency.
    • Suitable for scenarios with high data update frequency, as this method does not require any additional data maintenance.

    Binary search

    • Suitable for large datasets, with stable performance and a worst-case time complexity of \\(O(\\log n)\\).
    • Data volume cannot be too large, as storing arrays requires contiguous memory space.
    • Not suitable for scenarios with frequent data insertion and deletion, as maintaining a sorted array has high overhead.

    Hash-based search

    • Suitable for scenarios with high query performance requirements, with an average time complexity of \\(O(1)\\).
    • Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain the data in sorted order.
    • High dependence on hash functions and hash collision handling strategies, with significant risk of performance degradation.
    • Not suitable for excessively large data volumes, as hash tables require extra space to minimize collisions and thus provide good query performance.

    Tree search

    • Suitable for massive datasets, as tree nodes are stored non-contiguously in memory.
    • Suitable for scenarios that require maintaining ordered data or performing range searches.
    • During continuous node insertion and deletion, binary search trees may become skewed, degrading time complexity to \\(O(n)\\).
    • If AVL trees or red-black trees are used, all operations can consistently run in \\(O(\\log n)\\) time, though maintaining tree balance adds extra overhead.
    ","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   Summary","text":"","path":["Chapter 10. Searching","10.6   Summary"],"tags":[]},{"location":"chapter_searching/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Binary search relies on ordered data and searches by repeatedly halving the search interval. It requires the input data to be sorted and applies only to arrays or array-based data structures.
    • Brute-force search locates data by traversing the data structure. Linear search applies to arrays and linked lists, while breadth-first search and depth-first search apply to graphs and trees. These algorithms are broadly applicable and require no data preprocessing, but their relatively high time complexity is \\(O(n)\\).
    • Hash-based search, tree search, and binary search are efficient search methods that can quickly locate target elements in specific data structures. Such algorithms are highly efficient with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\), but typically require additional data structures.
    • In practice, we need to analyze factors such as data scale, search performance requirements, and data query and update frequency to choose the appropriate search method.
    • Linear search is suitable for small datasets or data that is updated frequently; binary search is suitable for large sorted datasets; hash-based search is suitable when high query efficiency is required and range queries are unnecessary; tree search is suitable for large dynamic datasets that must maintain order and support range queries.
    • Replacing linear search with hash-based search is a commonly used strategy to optimize runtime, reducing time complexity from \\(O(n)\\) to \\(O(1)\\).
    ","path":["Chapter 10. Searching","10.6   Summary"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"Chapter 11.   Sorting","text":"

    Abstract

    Sorting is like a magic key that transforms chaos into order, enabling us to understand and process data more efficiently.

    From simple ascending order to more complex classification schemes, sorting reveals the harmonious beauty of data.

    ","path":["Chapter 11. Sorting","Chapter 11.   Sorting"],"tags":[]},{"location":"chapter_sorting/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 11.1   Sorting Algorithm
    • 11.2   Selection Sort
    • 11.3   Bubble Sort
    • 11.4   Insertion Sort
    • 11.5   Quick Sort
    • 11.6   Merge Sort
    • 11.7   Heap Sort
    • 11.8   Bucket Sort
    • 11.9   Counting Sort
    • 11.10   Radix Sort
    • 11.11   Summary
    ","path":["Chapter 11. Sorting","Chapter 11.   Sorting"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   Bubble Sort","text":"

    Bubble sort sorts an array by continuously comparing and swapping adjacent elements. This process resembles bubbles rising from the bottom to the top, hence the name bubble sort.

    As shown in Figure 11-4, the bubbling process can be simulated using element swaps: starting from the leftmost end of the array and traversing to the right, compare each pair of adjacent elements, and if \"left element > right element\", swap them. After the traversal is complete, the largest element is moved to the rightmost end of the array.

    <1><2><3><4><5><6><7>

    Figure 11-4   Simulating bubble sort using element swaps

    ","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131-algorithm-flow","level":2,"title":"11.3.1   Algorithm Flow","text":"

    Assume the array has length \\(n\\). The steps of bubble sort are shown in Figure 11-5.

    1. First, perform \"bubbling\" on \\(n\\) elements, swapping the largest element of the array to its correct position.
    2. Next, perform \"bubbling\" on the remaining \\(n - 1\\) elements, swapping the second largest element to its correct position.
    3. And so on. After \\(n - 1\\) rounds of \"bubbling\", the largest \\(n - 1\\) elements have all been swapped to their correct positions.
    4. The only remaining element must be the smallest element, requiring no sorting, so the array sorting is complete.

    Figure 11-5   Bubble sort flow

    Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
    def bubble_sort(nums: list[int]):\n    \"\"\"Bubble sort\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [0, i]\n    for i in range(n - 1, 0, -1):\n        # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n
    bubble_sort.cpp
    /* Bubble sort */\nvoid bubbleSort(vector<int> &nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                // Using std::swap() function here\n                swap(nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
    bubble_sort.java
    /* Bubble sort */\nvoid bubbleSort(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and 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
    /* Bubble sort */\nvoid BubbleSort(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
    bubble_sort.go
    /* Bubble sort */\nfunc bubbleSort(nums []int) {\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n            }\n        }\n    }\n}\n
    bubble_sort.swift
    /* Bubble sort */\nfunc bubbleSort(nums: inout [Int]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swapAt(j, j + 1)\n            }\n        }\n    }\n}\n
    bubble_sort.js
    /* Bubble sort */\nfunction bubbleSort(nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and 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
    /* Bubble sort */\nfunction bubbleSort(nums: number[]): void {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and 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
    /* Bubble sort */\nvoid bubbleSort(List<int> nums) {\n  // Outer loop: unsorted range is [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and 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
    /* Bubble sort */\nfn bubble_sort(nums: &mut [i32]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swap(j, j + 1);\n            }\n        }\n    }\n}\n
    bubble_sort.c
    /* Bubble sort */\nvoid bubbleSort(int nums[], int size) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\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
    /* Bubble sort */\nfun bubbleSort(nums: IntArray) {\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and 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
    ### Bubble sort ###\ndef bubble_sort(nums)\n  n = nums.length\n  # Outer loop: unsorted range is [0, i]\n  for i in (n - 1).downto(1)\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132-efficiency-optimization","level":2,"title":"11.3.2   Efficiency Optimization","text":"

    We can observe that if no swaps occur during a round of \"bubbling\", the array is already sorted and the algorithm can return immediately. Therefore, we can add a flag flag to detect this situation and terminate as soon as it occurs.

    After this optimization, the worst-case and average-case time complexities of bubble sort remain \\(O(n^2)\\); however, when the input array is already sorted, the best-case time complexity becomes \\(O(n)\\).

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
    def bubble_sort_with_flag(nums: list[int]):\n    \"\"\"Bubble sort (flag optimization)\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [0, i]\n    for i in range(n - 1, 0, -1):\n        flag = False  # Initialize flag\n        # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n                flag = True  # Record element swap\n        if not flag:\n            break  # No elements were swapped in this round of \"bubbling\", exit directly\n
    bubble_sort.cpp
    /* Bubble sort (flag optimization)*/\nvoid bubbleSortWithFlag(vector<int> &nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        bool flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                // Using std::swap() function here\n                swap(nums[j], nums[j + 1]);\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag)\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.java
    /* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        boolean flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag)\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.cs
    /* Bubble sort (flag optimization) */\nvoid BubbleSortWithFlag(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        bool flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                flag = true;  // Record element swap\n            }\n        }\n        if (!flag) break;     // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.go
    /* Bubble sort (flag optimization) */\nfunc bubbleSortWithFlag(nums []int) {\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        flag := false // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n                flag = true // Record element swap\n            }\n        }\n        if flag == false { // No elements were swapped in this round of \"bubbling\", exit directly\n            break\n        }\n    }\n}\n
    bubble_sort.swift
    /* Bubble sort (flag optimization) */\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        var flag = false // Initialize flag\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swapAt(j, j + 1)\n                flag = true // Record element swap\n            }\n        }\n        if !flag { // No elements were swapped in this round of \"bubbling\", exit directly\n            break\n        }\n    }\n}\n
    bubble_sort.js
    /* Bubble sort (flag optimization) */\nfunction bubbleSortWithFlag(nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.ts
    /* Bubble sort (flag optimization) */\nfunction bubbleSortWithFlag(nums: number[]): void {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.dart
    /* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(List<int> nums) {\n  // Outer loop: unsorted range is [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    bool flag = false; // Initialize flag\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        flag = true; // Record element swap\n      }\n    }\n    if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n  }\n}\n
    bubble_sort.rs
    /* Bubble sort (flag optimization) */\nfn bubble_sort_with_flag(nums: &mut [i32]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        let mut flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swap(j, j + 1);\n                flag = true; // Record element swap\n            }\n        }\n        if !flag {\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n        };\n    }\n}\n
    bubble_sort.c
    /* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(int nums[], int size) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        bool flag = false;\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\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
    /* Bubble sort (flag optimization) */\nfun bubbleSortWithFlag(nums: IntArray) {\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        var flag = false // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                flag = true // Record element swap\n            }\n        }\n        if (!flag) break // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
    bubble_sort.rb
    ### Bubble sort (flag optimization) ###\ndef bubble_sort_with_flag(nums)\n  n = nums.length\n  # Outer loop: unsorted range is [0, i]\n  for i in (n - 1).downto(1)\n    flag = false # Initialize flag\n\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n        flag = true # Record element swap\n      end\n    end\n\n    break unless flag # No elements were swapped in this round of \"bubbling\", exit directly\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133-algorithm-characteristics","level":2,"title":"11.3.3   Algorithm Characteristics","text":"
    • Time complexity is \\(O(n^2)\\); adaptive: In successive rounds of \"bubbling\", the traversed portion of the array has lengths \\(n - 1\\), \\(n - 2\\), \\(\\dots\\), \\(2\\), \\(1\\), for a total of \\((n - 1) n / 2\\). After introducing the flag optimization, the best-case time complexity can reach \\(O(n)\\).
    • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
    • Stable sorting: Equal elements are not swapped during \"bubbling\".
    ","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   Bucket Sort","text":"

    The sorting algorithms discussed earlier are all comparison-based sorting algorithms, which sort by comparing the relative order of elements. The time complexity of such algorithms cannot beat \\(O(n \\log n)\\). Next, we will explore several non-comparison sorting algorithms, whose time complexity can be linear.

    Bucket sort is a typical application of the divide-and-conquer strategy. It works by creating a sequence of ordered buckets, each corresponding to a data range, and distributing the data evenly among them. The elements within each bucket are then sorted separately. Finally, all buckets are merged in order.

    ","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181-algorithm-flow","level":2,"title":"11.8.1   Algorithm Flow","text":"

    Consider an array of length \\(n\\), whose elements are floating-point numbers in the range \\([0, 1)\\). The flow of bucket sort is shown in Figure 11-13.

    1. Initialize \\(k\\) buckets and distribute the \\(n\\) elements into the \\(k\\) buckets.
    2. Sort each bucket separately (here we use the built-in sorting function of the programming language).
    3. Merge the results in order from smallest to largest bucket.

    Figure 11-13   Bucket sort algorithm flow

    The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py
    def bucket_sort(nums: list[float]):\n    \"\"\"Bucket sort\"\"\"\n    # Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    k = len(nums) // 2\n    buckets = [[] for _ in range(k)]\n    # 1. Distribute array elements into various buckets\n    for num in nums:\n        # Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        i = int(num * k)\n        # Add num to bucket i\n        buckets[i].append(num)\n    # 2. Sort each bucket\n    for bucket in buckets:\n        # Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort()\n    # 3. Traverse buckets to merge results\n    i = 0\n    for bucket in buckets:\n        for num in bucket:\n            nums[i] = num\n            i += 1\n
    bucket_sort.cpp
    /* Bucket sort */\nvoid bucketSort(vector<float> &nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    int k = nums.size() / 2;\n    vector<vector<float>> buckets(k);\n    // 1. Distribute array elements into various buckets\n    for (float num : nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = num * k;\n        // Add num to bucket bucket_idx\n        buckets[i].push_back(num);\n    }\n    // 2. Sort each bucket\n    for (vector<float> &bucket : buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        sort(bucket.begin(), bucket.end());\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nvoid bucketSort(float[] nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\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. Distribute array elements into various buckets\n    for (float num : nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = (int) (num * k);\n        // Add num to bucket i\n        buckets.get(i).add(num);\n    }\n    // 2. Sort each bucket\n    for (List<Float> bucket : buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        Collections.sort(bucket);\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nvoid BucketSort(float[] nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\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. Distribute array elements into various buckets\n    foreach (float num in nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = (int)(num * k);\n        // Add num to bucket i\n        buckets[i].Add(num);\n    }\n    // 2. Sort each bucket\n    foreach (List<float> bucket in buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.Sort();\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfunc bucketSort(nums []float64) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\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. Distribute array elements into various buckets\n    for _, num := range nums {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        i := int(num * float64(k))\n        // Add num to bucket i\n        buckets[i] = append(buckets[i], num)\n    }\n    // 2. Sort each bucket\n    for i := 0; i < k; i++ {\n        // Use built-in slice sorting function, can also be replaced with other sorting algorithms\n        sort.Float64s(buckets[i])\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfunc bucketSort(nums: inout [Double]) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    let k = nums.count / 2\n    var buckets = (0 ..< k).map { _ in [Double]() }\n    // 1. Distribute array elements into various buckets\n    for num in nums {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        let i = Int(num * Double(k))\n        // Add num to bucket i\n        buckets[i].append(num)\n    }\n    // 2. Sort each bucket\n    for i in buckets.indices {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        buckets[i].sort()\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfunction bucketSort(nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    const k = nums.length / 2;\n    const buckets = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Distribute array elements into various buckets\n    for (const num of nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        const i = Math.floor(num * k);\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for (const bucket of buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfunction bucketSort(nums: number[]): void {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    const k = nums.length / 2;\n    const buckets: number[][] = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Distribute array elements into various buckets\n    for (const num of nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        const i = Math.floor(num * k);\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for (const bucket of buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nvoid bucketSort(List<double> nums) {\n  // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n  int k = nums.length ~/ 2;\n  List<List<double>> buckets = List.generate(k, (index) => []);\n\n  // 1. Distribute array elements into various buckets\n  for (double _num in nums) {\n    // Input data range is [0, 1), use _num * k to map to index range [0, k-1]\n    int i = (_num * k).toInt();\n    // Add _num to bucket bucket_idx\n    buckets[i].add(_num);\n  }\n  // 2. Sort each bucket\n  for (List<double> bucket in buckets) {\n    bucket.sort();\n  }\n  // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nfn bucket_sort(nums: &mut [f64]) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    let k = nums.len() / 2;\n    let mut buckets = vec![vec![]; k];\n    // 1. Distribute array elements into various buckets\n    for &num in nums.iter() {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        let i = (num * k as f64) as usize;\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for bucket in &mut buckets {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n    }\n    // 3. Traverse buckets to merge results\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
    /* Bucket sort */\nvoid bucketSort(float nums[], int n) {\n    int k = n / 2;                                 // Initialize k = n/2 buckets\n    int *sizes = malloc(k * sizeof(int));          // Record each bucket's size\n    float **buckets = malloc(k * sizeof(float *)); // Array of dynamic arrays (buckets)\n    // Pre-allocate sufficient space for each bucket\n    for (int i = 0; i < k; ++i) {\n        buckets[i] = (float *)malloc(n * sizeof(float));\n        sizes[i] = 0;\n    }\n    // 1. Distribute array elements into various buckets\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. Sort each bucket\n    for (int i = 0; i < k; ++i) {\n        qsort(buckets[i], sizes[i], sizeof(float), compare);\n    }\n    // 3. Merge sorted buckets\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        // Free memory\n        free(buckets[i]);\n    }\n}\n
    bucket_sort.kt
    /* Bucket sort */\nfun bucketSort(nums: FloatArray) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\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. Distribute array elements into various buckets\n    for (num in nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        val i = (num * k).toInt()\n        // Add num to bucket i\n        buckets[i].add(num)\n    }\n    // 2. Sort each bucket\n    for (bucket in buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort()\n    }\n    // 3. Traverse buckets to merge results\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
    ### Bucket sort ###\ndef bucket_sort(nums)\n  # Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n  k = nums.length / 2\n  buckets = Array.new(k) { [] }\n\n  # 1. Distribute array elements into various buckets\n  nums.each do |num|\n    # Input data range is [0, 1), use num * k to map to index range [0, k-1]\n    i = (num * k).to_i\n    # Add num to bucket i\n    buckets[i] << num\n  end\n\n  # 2. Sort each bucket\n  buckets.each do |bucket|\n    # Use built-in sorting function, can also replace with other sorting algorithms\n    bucket.sort!\n  end\n\n  # 3. Traverse buckets to merge results\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":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182-algorithm-characteristics","level":2,"title":"11.8.2   Algorithm Characteristics","text":"

    Bucket sort is suitable for processing very large datasets. For example, suppose the input contains 1 million elements, and limited memory prevents the system from loading all of them at once. In that case, the data can be divided into 1000 buckets, each bucket can be sorted separately, and the results can then be merged.

    • Time complexity is \\(O(n + k)\\): Assuming the elements are evenly distributed across the buckets, each bucket contains \\(\\frac{n}{k}\\) elements. If sorting a single bucket takes \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) time, then sorting all buckets takes \\(O(n \\log\\frac{n}{k})\\) time. When the number of buckets \\(k\\) is relatively large, the time complexity approaches \\(O(n)\\). Merging the results requires traversing all buckets and elements, which takes \\(O(n + k)\\) time. In the worst case, all data is placed into a single bucket, and sorting that bucket takes \\(O(n^2)\\) time.
    • Space complexity is \\(O(n + k)\\), and bucket sort is not in-place: It requires extra space for \\(k\\) buckets and a total of \\(n\\) elements.
    • Whether bucket sort is stable depends on whether the algorithm for sorting elements within buckets is stable.
    ","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183-how-to-achieve-even-distribution","level":2,"title":"11.8.3   How to Achieve Even Distribution","text":"

    In theory, bucket sort can achieve \\(O(n)\\) time complexity. The key is to distribute the elements evenly across the buckets, because real-world data is often not uniformly distributed. For example, suppose we want to divide all products on Taobao evenly into 10 buckets by price range, but the price distribution is uneven: there are many products priced below 100 yuan and very few priced above 1000 yuan. If the price range is divided evenly into 10 intervals, the numbers of products in the buckets will differ greatly.

    To achieve a more even distribution, we can first choose a rough boundary and partition the data into 3 buckets. After that, buckets containing more products can be further divided into 3 buckets until the numbers of elements in all buckets are roughly equal.

    As shown in Figure 11-14, this method essentially builds a recursion tree whose goal is to make the leaf nodes as balanced as possible. Of course, the data does not have to be split into 3 buckets in every round; the specific partitioning strategy can be chosen flexibly based on the characteristics of the data.

    Figure 11-14   Recursively dividing buckets

    If we know the probability distribution of product prices in advance, we can set the price boundaries for each bucket according to that distribution. Notably, the data distribution does not need to be measured exactly; it can also be approximated with a probability model chosen to fit the characteristics of the data.

    As shown in Figure 11-15, we assume that product prices follow a normal distribution, which allows us to reasonably set price intervals to evenly distribute products to each bucket.

    Figure 11-15   Dividing buckets based on probability distribution

    ","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   Counting Sort","text":"

    Counting sort sorts by counting the occurrences of elements and is typically applied to integer arrays.

    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191-simple-implementation","level":2,"title":"11.9.1   Simple Implementation","text":"

    Let's start with a simple example. Given an array nums of length \\(n\\), where the elements are all \"non-negative integers\", the overall flow of counting sort is shown in Figure 11-16.

    1. Traverse the array to find the largest number, denoted as \\(m\\), and then create an auxiliary array counter of length \\(m + 1\\).
    2. Use counter to count how many times each number appears in nums, where counter[num] stores the number of occurrences of num. This is simple: traverse nums (denote the current number by num) and increment counter[num] by \\(1\\) each time.
    3. Because the indices of counter are naturally ordered, the numbers are effectively already sorted. Next, traverse counter and write the numbers back into nums in ascending order according to their occurrence counts.

    Figure 11-16   Counting sort flow

    The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
    def counting_sort_naive(nums: list[int]):\n    \"\"\"Counting sort\"\"\"\n    # Simple implementation, cannot be used for sorting objects\n    # 1. Count the maximum element m in the array\n    m = 0\n    for num in nums:\n        m = max(m, num)\n    # 2. Count the occurrence of each number\n    # counter[num] represents the occurrence of num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(vector<int> &nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid CountingSortNaive(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunc countingSortNaive(nums []int) {\n    // 1. Count the maximum element m in the array\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunc countingSortNaive(nums: inout [Int]) {\n    // 1. Count the maximum element m in the array\n    let m = nums.max()!\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunction countingSortNaive(nums) {\n    // 1. Count the maximum element m in the array\n    let m = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunction countingSortNaive(nums: number[]): void {\n    // 1. Count the maximum element m in the array\n    let m: number = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(List<int> nums) {\n  // 1. Count the maximum element m in the array\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Count the occurrence of each number\n  // counter[_num] represents occurrence count of _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfn counting_sort_naive(nums: &mut [i32]) {\n    // 1. Count the maximum element m in the array\n    let m = *nums.iter().max().unwrap();\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of 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. Traverse counter, filling each element back into the original array 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
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(int nums[], int size) {\n    // 1. Count the maximum element m in the array\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. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int *counter = calloc(m + 1, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array 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. Free memory\n    free(counter);\n}\n
    counting_sort.kt
    /* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfun countingSortNaive(nums: IntArray) {\n    // 1. Count the maximum element m in the array\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Traverse counter, filling each element back into the original array 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
    ### Counting sort ###\ndef counting_sort_naive(nums)\n  # Simple implementation, cannot be used for sorting objects\n  # 1. Count the maximum element m in the array\n  m = 0\n  nums.each { |num| m = [m, num].max }\n  # 2. Count the occurrence of each number\n  # counter[num] represents the occurrence of num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Traverse counter, filling each element back into the original array 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

    Connection between counting sort and bucket sort

    From the perspective of bucket sort, each index of the counting array counter can be viewed as a bucket, and the counting process can be seen as distributing elements into their corresponding buckets. Essentially, counting sort is a special case of bucket sort for integer data.

    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192-complete-implementation","level":2,"title":"11.9.2   Complete Implementation","text":"

    Observant readers may have noticed that if the input consists of objects, step 3. above no longer works. Suppose the input consists of product objects and we want to sort them by price (a member variable of the class); the above algorithm can only produce the sorted order of the prices themselves.

    So how can we obtain the sorted order of the original data? We first compute the prefix sums of counter. As the name suggests, the prefix sum at index i, prefix[i], equals the sum of the elements from index 0 through i:

    \\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\]

    The prefix sum has a clear interpretation: prefix[num] - 1 gives the index of the last occurrence of element num in the result array res. This information is crucial because it tells us where each element should be placed in the result array. Next, we traverse the original array nums in reverse, and for each element num, perform the following two steps.

    1. Place num at index prefix[num] - 1 of the array res.
    2. Decrease the prefix sum prefix[num] by \\(1\\) to get the index for the next placement of num.

    After the traversal is complete, the array res contains the sorted result, and finally res is used to overwrite the original array nums. The complete counting sort flow is shown in Figure 11-17.

    <1><2><3><4><5><6><7><8>

    Figure 11-17   Counting sort steps

    The counting sort implementation is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
    def counting_sort(nums: list[int]):\n    \"\"\"Counting sort\"\"\"\n    # Complete implementation, can sort objects and is a stable sort\n    # 1. Count the maximum element m in the array\n    m = max(nums)\n    # 2. Count the occurrence of each number\n    # counter[num] represents the occurrence of num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    # counter[num]-1 is the last index where num appears in res\n    for i in range(m):\n        counter[i + 1] += counter[i]\n    # 4. Traverse nums in reverse order, placing each element into the result array res\n    # Initialize the array res to record results\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  # Place num at the corresponding index\n        counter[num] -= 1  # Decrement the prefix sum by 1, getting the next index to place num\n    # Use result array res to overwrite the original array nums\n    for i in range(n):\n        nums[i] = res[i]\n
    counting_sort.cpp
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(vector<int> &nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--;              // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    nums = res;\n}\n
    counting_sort.java
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
    counting_sort.cs
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid CountingSort(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
    counting_sort.go
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunc countingSort(nums []int) {\n    // 1. Count the maximum element m in the array\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i := 0; i < m; i++ {\n        counter[i+1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    n := len(nums)\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        num := nums[i]\n        // Place num at the corresponding index\n        res[counter[num]-1] = num\n        // Decrement the prefix sum by 1, getting the next index to place num\n        counter[num]--\n    }\n    // Use result array res to overwrite the original array nums\n    copy(nums, res)\n}\n
    counting_sort.swift
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunc countingSort(nums: inout [Int]) {\n    // 1. Count the maximum element m in the array\n    let m = nums.max()!\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i in 0 ..< m {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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 // Place num at the corresponding index\n        counter[num] -= 1 // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n
    counting_sort.js
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunction countingSort(nums) {\n    // 1. Count the maximum element m in the array\n    let m = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
    counting_sort.ts
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunction countingSort(nums: number[]): void {\n    // 1. Count the maximum element m in the array\n    let m: number = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
    counting_sort.dart
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(List<int> nums) {\n  // 1. Count the maximum element m in the array\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Count the occurrence of each number\n  // counter[_num] represents occurrence count of _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n  // That is, counter[_num]-1 is the last occurrence index of _num in res\n  for (int i = 0; i < m; i++) {\n    counter[i + 1] += counter[i];\n  }\n  // 4. Traverse nums in reverse order, placing each element into the result array res\n  // Initialize the array res to record results\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; // Place _num at corresponding index\n    counter[_num]--; // Decrement prefix sum by 1 to get next placement index for _num\n  }\n  // Use result array res to overwrite the original array nums\n  nums.setAll(0, res);\n}\n
    counting_sort.rs
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfn counting_sort(nums: &mut [i32]) {\n    // 1. Count the maximum element m in the array\n    let m = *nums.iter().max().unwrap() as usize;\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    let mut counter = vec![0; m + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i in 0..m {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num as usize] -= 1; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    nums.copy_from_slice(&res)\n}\n
    counting_sort.c
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(int nums[], int size) {\n    // 1. Count the maximum element m in the array\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. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int *counter = calloc(m, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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; // Place num at the corresponding index\n        counter[num]--;              // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    memcpy(nums, res, size * sizeof(int));\n    // 5. Free memory\n    free(res);\n    free(counter);\n}\n
    counting_sort.kt
    /* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfun countingSort(nums: IntArray) {\n    // 1. Count the maximum element m in the array\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (i in 0..<m) {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\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 // Place num at the corresponding index\n        counter[num]-- // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (i in 0..<n) {\n        nums[i] = res[i]\n    }\n}\n
    counting_sort.rb
    ### Counting sort ###\ndef counting_sort(nums)\n  # Complete implementation, can sort objects and is a stable sort\n  # 1. Count the maximum element m in the array\n  m = nums.max\n  # 2. Count the occurrence of each number\n  # counter[num] represents the occurrence of num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n  # counter[num]-1 is the last index where num appears in res\n  (0...m).each { |i| counter[i + 1] += counter[i] }\n  # 4. Traverse nums in reverse, fill elements into result array res\n  # Initialize the array res to record results\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 # Place num at the corresponding index\n    counter[num] -= 1 # Decrement the prefix sum by 1, getting the next index to place num\n  end\n  # Use result array res to overwrite the original array nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n
    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193-algorithm-characteristics","level":2,"title":"11.9.3   Algorithm Characteristics","text":"
    • Time complexity is \\(O(n + m)\\), and counting sort is non-adaptive: Traversing nums and counter both takes linear time. In general, when \\(n \\gg m\\), the time complexity approaches \\(O(n)\\).
    • Space complexity of \\(O(n + m)\\), non-in-place sorting: Uses arrays res and counter of lengths \\(n\\) and \\(m\\) respectively.
    • Stable sorting: Since elements are filled into res in a \"right-to-left\" order, traversing nums in reverse can avoid changing the relative positions of equal elements, thereby achieving stable sorting. In fact, traversing nums in forward order can also yield correct sorting results, but the result would be unstable.
    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194-limitations","level":2,"title":"11.9.4   Limitations","text":"

    At this point, you might think counting sort is quite ingenious because it achieves efficient sorting simply by counting occurrences. However, the prerequisites for using counting sort are fairly restrictive.

    Counting sort is only applicable to non-negative integers. To apply it to other types of data, you must ensure that they can be converted to non-negative integers without changing the relative ordering of the elements. For example, for an integer array containing negative numbers, you can first add a constant to every number to shift them into the non-negative range, and then shift them back after sorting.

    Counting sort is well suited to cases with many elements but a small value range. For example, in the above scenario, \\(m\\) cannot be too large; otherwise, it consumes too much space. And when \\(n \\ll m\\), counting sort takes \\(O(m)\\) time, which may be slower than sorting algorithms with \\(O(n \\log n)\\) time complexity.

    ","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   Heap Sort","text":"

    Tip

    Before reading this section, please ensure you have completed the \"Heap\" chapter.

    Heap sort is an efficient sorting algorithm based on the heap data structure. We can implement heap sort using the heap construction and element removal operations introduced earlier.

    1. Input the array and build a min-heap, at which point the smallest element is at the heap top.
    2. Continuously perform element removal operations and record the removed elements in order to obtain a sequence sorted in ascending order.

    Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method.

    ","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171-algorithm-flow","level":2,"title":"11.7.1   Algorithm Flow","text":"

    Assume the array length is \\(n\\). The flow of heap sort is shown in Figure 11-12.

    1. Input the array and build a max-heap. After completion, the largest element is at the heap top.
    2. Swap the heap top element (first element) with the heap bottom element (last element). After the swap is complete, reduce the heap length by \\(1\\) and increase the count of sorted elements by \\(1\\).
    3. Starting from the heap top element, perform a top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored.
    4. Repeat steps 2. and 3. After \\(n - 1\\) rounds, the array is sorted.

    Tip

    In fact, the element removal operation also includes steps 2. and 3., with the additional step of removing the element.

    <1><2><3><4><5><6><7><8><9><10><11><12>

    Figure 11-12   Heap sort steps

    In the code below, we use the same sift_down() function for top-to-bottom heapify as in the \"Heap\" chapter. It is worth noting that since the heap length decreases as the largest element is extracted, we need to add a length parameter \\(n\\) to sift_down() to specify the current effective length of the heap. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py
    def sift_down(nums: list[int], n: int, i: int):\n    \"\"\"Heap length is n, start heapifying node i, from top to bottom\"\"\"\n    while True:\n        # Determine the largest node among i, l, r, noted as 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        # If node i is the largest or indices l, r are out of bounds, no further heapification needed, break\n        if ma == i:\n            break\n        # Swap two nodes\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # Loop downwards heapification\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"Heap sort\"\"\"\n    # Build heap operation: heapify all nodes except leaves\n    for i in range(len(nums) // 2 - 1, -1, -1):\n        sift_down(nums, len(nums), i)\n    # Extract the largest element from the heap and repeat for n-1 rounds\n    for i in range(len(nums) - 1, 0, -1):\n        # Swap the root node with the rightmost leaf node (swap the first element with the last element)\n        nums[0], nums[i] = nums[i], nums[0]\n        # Start heapifying the root node, from top to bottom\n        sift_down(nums, i, 0)\n
    heap_sort.cpp
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(vector<int> &nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i) {\n            break;\n        }\n        // Swap two nodes\n        swap(nums[i], nums[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(vector<int> &nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n        siftDown(nums, nums.size(), i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.size() - 1; i > 0; --i) {\n        // Delete node\n        swap(nums[0], nums[i]);\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.java
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(int[] nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(int[] nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.length / 2 - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.cs
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid SiftDown(int[] nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid HeapSort(int[] nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.Length / 2 - 1; i >= 0; i--) {\n        SiftDown(nums, nums.Length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Delete node\n        (nums[i], nums[0]) = (nums[0], nums[i]);\n        // Start heapifying the root node, from top to bottom\n        SiftDown(nums, i, 0);\n    }\n}\n
    heap_sort.go
    /* Heap length is n, start heapifying node i, from top to bottom */\nfunc siftDown(nums *[]int, n, i int) {\n    for true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfunc heapSort(nums *[]int) {\n    // Build heap operation: heapify all nodes except leaves\n    for i := len(*nums)/2 - 1; i >= 0; i-- {\n        siftDown(nums, len(*nums), i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i := len(*nums) - 1; i > 0; i-- {\n        // Delete node\n        (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0)\n    }\n}\n
    heap_sort.swift
    /* Heap length is n, start heapifying node i, from top to bottom */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n    var i = i\n    while true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        nums.swapAt(i, ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfunc heapSort(nums: inout [Int]) {\n    // Build heap operation: heapify all nodes except leaves\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    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i in nums.indices.dropFirst().reversed() {\n        // Delete node\n        nums.swapAt(0, i)\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums: &nums, n: i, i: 0)\n    }\n}\n
    heap_sort.js
    /* Heap length is n, start heapifying node i, from top to bottom */\nfunction siftDown(nums, n, i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma === i) {\n            break;\n        }\n        // Swap two nodes\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfunction heapSort(nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.ts
    /* Heap length is n, start heapifying node i, from top to bottom */\nfunction siftDown(nums: number[], n: number, i: number): void {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma === i) {\n            break;\n        }\n        // Swap two nodes\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfunction heapSort(nums: number[]): void {\n    // Build heap operation: heapify all nodes except leaves\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.dart
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(List<int> nums, int n, int i) {\n  while (true) {\n    // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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    // Swap two nodes\n    if (ma == i) break;\n    // Swap two nodes\n    int temp = nums[i];\n    nums[i] = nums[ma];\n    nums[ma] = temp;\n    // Loop downwards heapification\n    i = ma;\n  }\n}\n\n/* Heap sort */\nvoid heapSort(List<int> nums) {\n  // Build heap operation: heapify all nodes except leaves\n  for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {\n    siftDown(nums, nums.length, i);\n  }\n  // Extract the largest element from the heap and repeat for n-1 rounds\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Delete node\n    int tmp = nums[0];\n    nums[0] = nums[i];\n    nums[i] = tmp;\n    // Start heapifying the root node, from top to bottom\n    siftDown(nums, i, 0);\n  }\n}\n
    heap_sort.rs
    /* Heap length is n, start heapifying node i, from top to bottom */\nfn sift_down(nums: &mut [i32], n: usize, mut i: usize) {\n    loop {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if ma == i {\n            break;\n        }\n        // Swap two nodes\n        nums.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfn heap_sort(nums: &mut [i32]) {\n    // Build heap operation: heapify all nodes except leaves\n    for i in (0..nums.len() / 2).rev() {\n        sift_down(nums, nums.len(), i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i in (1..nums.len()).rev() {\n        // Delete node\n        nums.swap(0, i);\n        // Start heapifying the root node, from top to bottom\n        sift_down(nums, i, 0);\n    }\n}\n
    heap_sort.c
    /* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(int nums[], int n, int i) {\n    while (1) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i) {\n            break;\n        }\n        // Swap two nodes\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(int nums[], int n) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = n / 2 - 1; i >= 0; --i) {\n        siftDown(nums, n, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = n - 1; i > 0; --i) {\n        // Delete node\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
    heap_sort.kt
    /* Heap length is n, start heapifying node i, from top to bottom */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n    var i = li\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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        // Swap two nodes\n        if (ma == i) \n            break\n        // Swap two nodes\n        val temp = nums[i]\n        nums[i] = nums[ma]\n        nums[ma] = temp\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfun heapSort(nums: IntArray) {\n    // Build heap operation: heapify all nodes except leaves\n    for (i in nums.size / 2 - 1 downTo 0) {\n        siftDown(nums, nums.size, i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (i in nums.size - 1 downTo 1) {\n        // Delete node\n        val temp = nums[0]\n        nums[0] = nums[i]\n        nums[i] = temp\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0)\n    }\n}\n
    heap_sort.rb
    ### Heap length is n, heapify from node i, top to bottom ###\ndef sift_down(nums, n, i)\n  while true\n    # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\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    # Swap two nodes\n    break if ma == i\n    # Swap two nodes\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # Loop downwards heapification\n    i = ma\n  end\nend\n\n### Heap sort ###\ndef heap_sort(nums)\n  # Build heap operation: heapify all nodes except leaves\n  (nums.length / 2 - 1).downto(0) do |i|\n    sift_down(nums, nums.length, i)\n  end\n  # Extract the largest element from the heap and repeat for n-1 rounds\n  (nums.length - 1).downto(1) do |i|\n    # Delete node\n    nums[0], nums[i] = nums[i], nums[0]\n    # Start heapifying the root node, from top to bottom\n    sift_down(nums, i, 0)\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172-algorithm-characteristics","level":2,"title":"11.7.2   Algorithm Characteristics","text":"
    • Time complexity is \\(O(n \\log n)\\); heap sort is non-adaptive: Heap construction takes \\(O(n)\\) time. Extracting the largest element from the heap takes \\(O(\\log n)\\) time, and this is repeated for a total of \\(n - 1\\) rounds.
    • Space complexity is \\(O(1)\\); heap sort is in-place: A few pointer variables use \\(O(1)\\) space. Element swapping and heapify are both performed on the original array.
    • Unstable sorting: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change.
    ","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   Insertion Sort","text":"

    Insertion sort is a simple sorting algorithm that works very similarly to the process of manually sorting a deck of cards.

    Specifically, we select a base element from the unsorted portion, compare it one by one with the elements in the sorted portion to its left, and insert it into the correct position.

    Figure 11-6 illustrates how an element is inserted into an array. Let the base element be base. We need to shift all elements between the target index and base one position to the right, and then assign base to the target index.

    Figure 11-6   Single insertion operation

    ","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141-algorithm-flow","level":2,"title":"11.4.1   Algorithm Flow","text":"

    The overall flow of insertion sort is shown in Figure 11-7.

    1. Initially, the first element of the array is already sorted.
    2. Select the second element of the array as base, and after inserting it into the correct position, the first 2 elements of the array are sorted.
    3. Select the third element as base, and after inserting it into the correct position, the first 3 elements of the array are sorted.
    4. And so on. In the last round, select the last element as base, and after inserting it into the correct position, all elements are sorted.

    Figure 11-7   Insertion sort flow

    Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py
    def insertion_sort(nums: list[int]):\n    \"\"\"Insertion sort\"\"\"\n    # Outer loop: sorted interval is [0, i-1]\n    for i in range(1, len(nums)):\n        base = nums[i]\n        j = i - 1\n        # Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0 and nums[j] > base:\n            nums[j + 1] = nums[j]  # Move nums[j] to the right by one position\n            j -= 1\n        nums[j + 1] = base  # Assign base to the correct position\n
    insertion_sort.cpp
    /* Insertion sort */\nvoid insertionSort(vector<int> &nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.size(); i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
    insertion_sort.java
    /* Insertion sort */\nvoid insertionSort(int[] nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.length; i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base;        // Assign base to the correct position\n    }\n}\n
    insertion_sort.cs
    /* Insertion sort */\nvoid InsertionSort(int[] nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.Length; i++) {\n        int bas = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > bas) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = bas;         // Assign base to the correct position\n    }\n}\n
    insertion_sort.go
    /* Insertion sort */\nfunc insertionSort(nums []int) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i := 1; i < len(nums); i++ {\n        base := nums[i]\n        j := i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        for j >= 0 && nums[j] > base {\n            nums[j+1] = nums[j] // Move nums[j] to the right by one position\n            j--\n        }\n        nums[j+1] = base // Assign base to the correct position\n    }\n}\n
    insertion_sort.swift
    /* Insertion sort */\nfunc insertionSort(nums: inout [Int]) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i in nums.indices.dropFirst() {\n        let base = nums[i]\n        var j = i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0, nums[j] > base {\n            nums[j + 1] = nums[j] // Move nums[j] to the right by one position\n            j -= 1\n        }\n        nums[j + 1] = base // Assign base to the correct position\n    }\n}\n
    insertion_sort.js
    /* Insertion sort */\nfunction insertionSort(nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        let base = nums[i],\n            j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
    insertion_sort.ts
    /* Insertion sort */\nfunction insertionSort(nums: number[]): void {\n    // Outer loop: sorted interval is [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        const base = nums[i];\n        let j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
    insertion_sort.dart
    /* Insertion sort */\nvoid insertionSort(List<int> nums) {\n  // Outer loop: sorted interval is [0, i-1]\n  for (int i = 1; i < nums.length; i++) {\n    int base = nums[i], j = i - 1;\n    // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n    while (j >= 0 && nums[j] > base) {\n      nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n      j--;\n    }\n    nums[j + 1] = base; // Assign base to the correct position\n  }\n}\n
    insertion_sort.rs
    /* Insertion sort */\nfn insertion_sort(nums: &mut [i32]) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i in 1..nums.len() {\n        let (base, mut j) = (nums[i], (i - 1) as i32);\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0 && nums[j as usize] > base {\n            nums[(j + 1) as usize] = nums[j as usize]; // Move nums[j] to the right by one position\n            j -= 1;\n        }\n        nums[(j + 1) as usize] = base; // Assign base to the correct position\n    }\n}\n
    insertion_sort.c
    /* Insertion sort */\nvoid insertionSort(int nums[], int size) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < size; i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            // Move nums[j] to the right by one position\n            nums[j + 1] = nums[j];\n            j--;\n        }\n        // Assign base to the correct position\n        nums[j + 1] = base;\n    }\n}\n
    insertion_sort.kt
    /* Insertion sort */\nfun insertionSort(nums: IntArray) {\n    // Outer loop: sorted elements are 1, 2, ..., n\n    for (i in nums.indices) {\n        val base = nums[i]\n        var j = i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j] // Move nums[j] to the right by one position\n            j--\n        }\n        nums[j + 1] = base        // Assign base to the correct position\n    }\n}\n
    insertion_sort.rb
    ### Insertion sort ###\ndef insertion_sort(nums)\n  n = nums.length\n  # Outer loop: sorted interval is [0, i-1]\n  for i in 1...n\n    base = nums[i]\n    j = i - 1\n    # Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n    while j >= 0 && nums[j] > base\n      nums[j + 1] = nums[j] # Move nums[j] to the right by one position\n      j -= 1\n    end\n    nums[j + 1] = base # Assign base to the correct position\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142-algorithm-characteristics","level":2,"title":"11.4.2   Algorithm Characteristics","text":"
    • Time complexity of \\(O(n^2)\\), adaptive sorting: In the worst case, the insertion operations require \\(n - 1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), and \\(1\\) iterations, respectively, summing to \\((n - 1) n / 2\\), so the time complexity is \\(O(n^2)\\). When the data is already sorted, each insertion operation terminates early. When the input array is completely sorted, insertion sort achieves its best-case time complexity of \\(O(n)\\).
    • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
    • Stable sorting: During insertion, we place elements to the right of equal elements, so their relative order is unchanged.
    ","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143-advantages-of-insertion-sort","level":2,"title":"11.4.3   Advantages of Insertion Sort","text":"

    The time complexity of insertion sort is \\(O(n^2)\\), while the time complexity of quick sort, which we will learn about next, is \\(O(n \\log n)\\). Although insertion sort has a higher time complexity, it is usually faster on small datasets.

    This conclusion is similar to the one about when linear search and binary search are applicable. Algorithms such as quick sort, with \\(O(n \\log n)\\) complexity, are divide-and-conquer sorting algorithms and often involve more primitive operations. When the dataset is small, the values of \\(n^2\\) and \\(n \\log n\\) are relatively close, so asymptotic complexity does not dominate; instead, the number of primitive operations per round becomes the deciding factor.

    In fact, the built-in sorting functions of many programming languages (such as Java) use insertion sort. The general idea is: for large arrays, use divide-and-conquer sorting algorithms such as quick sort; for short arrays, use insertion sort directly.

    Although bubble sort, selection sort, and insertion sort all have a time complexity of \\(O(n^2)\\), in actual situations, insertion sort is used significantly more frequently than bubble sort and selection sort, mainly for the following reasons.

    • Bubble sort is implemented through element swaps, which require a temporary variable and involve 3 primitive operations; insertion sort is implemented through element assignment and requires only 1 primitive operation. Therefore, bubble sort usually has higher computational overhead than insertion sort.
    • Selection sort has a time complexity of \\(O(n^2)\\) in any case. If given a set of partially ordered data, insertion sort is usually more efficient than selection sort.
    • Selection sort is unstable and cannot be applied to multi-level sorting.
    ","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   Merge Sort","text":"

    Merge sort is a sorting algorithm based on a divide-and-conquer strategy, consisting of the \"divide\" and \"merge\" phases shown in Figure 11-10.

    1. Divide phase: Recursively split the array at the midpoint, reducing the problem of sorting a long array to the problem of sorting shorter arrays.
    2. Merge phase: When a sub-array has length 1, stop dividing and start merging, continuously combining the shorter sorted sub-arrays on the left and right into a longer sorted array until the process is complete.

    Figure 11-10   Divide and merge phases of merge sort

    ","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161-algorithm-flow","level":2,"title":"11.6.1   Algorithm Flow","text":"

    As shown in Figure 11-11, the \"divide phase\" recursively splits the array from the midpoint into two sub-arrays from top to bottom.

    1. Calculate the array midpoint mid, recursively divide the left sub-array (interval [left, mid]) and right sub-array (interval [mid + 1, right]).
    2. Repeat step 1. recursively until a sub-array has length 1.

    The \"merge phase\" merges the left and right sub-arrays into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, so every sub-array involved in this phase is already sorted.

    <1><2><3><4><5><6><7><8><9><10>

    Figure 11-11   Merge sort steps

    The recursive order of merge sort is consistent with the post-order traversal of a binary tree.

    • Post-order traversal: First recursively traverse the left subtree, then recursively traverse the right subtree, and finally process the root node.
    • Merge sort: First recursively process the left sub-array, then recursively process the right sub-array, and finally perform the merge.

    The implementation of merge sort is shown in the code below. Note that the interval to be merged in nums is [left, right], while the corresponding interval in tmp is [0, right - left].

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py
    def merge(nums: list[int], left: int, mid: int, right: int):\n    \"\"\"Merge left subarray and right subarray\"\"\"\n    # Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    # Create a temporary array tmp to store the merged results\n    tmp = [0] * (right - left + 1)\n    # Initialize the start indices of the left and right subarrays\n    i, j, k = left, mid + 1, 0\n    # While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    # Copy the remaining elements of the left and right subarrays into the temporary array\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    # Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\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    \"\"\"Merge sort\"\"\"\n    # Termination condition\n    if left >= right:\n        return  # Terminate recursion when subarray length is 1\n    # Divide and conquer stage\n    mid = (left + right) // 2  # Calculate midpoint\n    merge_sort(nums, left, mid)  # Recursively process the left subarray\n    merge_sort(nums, mid + 1, right)  # Recursively process the right subarray\n    # Merge stage\n    merge(nums, left, mid, right)\n
    merge_sort.cpp
    /* Merge left subarray and right subarray */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    vector<int> tmp(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.size(); k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    mergeSort(nums, left, mid);      // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.java
    /* Merge left subarray and right subarray */\nvoid merge(int[] nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int[] tmp = new int[right - left + 1];\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid mergeSort(int[] nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2; // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.cs
    /* Merge left subarray and right subarray */\nvoid Merge(int[] nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int[] tmp = new int[right - left + 1];\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.Length; ++k) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid MergeSort(int[] nums, int left, int right) {\n    // Termination condition\n    if (left >= right) return;       // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    MergeSort(nums, left, mid);      // Recursively process the left subarray\n    MergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    Merge(nums, left, mid, right);\n}\n
    merge_sort.go
    /* Merge left subarray and right subarray */\nfunc merge(nums []int, left, mid, right int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    tmp := make([]int, right-left+1)\n    // Initialize the start indices of the left and right subarrays\n    i, j, k := left, mid+1, 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\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    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k := 0; k < len(tmp); k++ {\n        nums[left+k] = tmp[k]\n    }\n}\n\n/* Merge sort */\nfunc mergeSort(nums []int, left, right int) {\n    // Termination condition\n    if left >= right {\n        return\n    }\n    // Divide and conquer stage\n    mid := left + (right - left) / 2\n    mergeSort(nums, left, mid)\n    mergeSort(nums, mid+1, right)\n    // Merge stage\n    merge(nums, left, mid, right)\n}\n
    merge_sort.swift
    /* Merge left subarray and right subarray */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    var tmp = Array(repeating: 0, count: right - left + 1)\n    // Initialize the start indices of the left and right subarrays\n    var i = left, j = mid + 1, k = 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\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    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in tmp.indices {\n        nums[left + k] = tmp[k]\n    }\n}\n\n/* Merge sort */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n    // Termination condition\n    if left >= right { // Terminate recursion when subarray length is 1\n        return\n    }\n    // Divide and conquer stage\n    let mid = left + (right - left) / 2 // Calculate midpoint\n    mergeSort(nums: &nums, left: left, right: mid) // Recursively process the left subarray\n    mergeSort(nums: &nums, left: mid + 1, right: right) // Recursively process the right subarray\n    // Merge stage\n    merge(nums: &nums, left: left, mid: mid, right: right)\n}\n
    merge_sort.js
    /* Merge left subarray and right subarray */\nfunction merge(nums, left, mid, right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    const tmp = new Array(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfunction mergeSort(nums, left, right) {\n    // Termination condition\n    if (left >= right) return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.ts
    /* Merge left subarray and right subarray */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    const tmp = new Array(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n    // Termination condition\n    if (left >= right) return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.dart
    /* Merge left subarray and right subarray */\nvoid merge(List<int> nums, int left, int mid, int right) {\n  // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n  // Create a temporary array tmp to store the merged results\n  List<int> tmp = List.filled(right - left + 1, 0);\n  // Initialize the start indices of the left and right subarrays\n  int i = left, j = mid + 1, k = 0;\n  // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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  // Copy the remaining elements of the left and right subarrays into the temporary array\n  while (i <= mid) {\n    tmp[k++] = nums[i++];\n  }\n  while (j <= right) {\n    tmp[k++] = nums[j++];\n  }\n  // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n  for (k = 0; k < tmp.length; k++) {\n    nums[left + k] = tmp[k];\n  }\n}\n\n/* Merge sort */\nvoid mergeSort(List<int> nums, int left, int right) {\n  // Termination condition\n  if (left >= right) return; // Terminate recursion when subarray length is 1\n  // Divide and conquer stage\n  int mid = left + (right - left) ~/ 2; // Calculate midpoint\n  mergeSort(nums, left, mid); // Recursively process the left subarray\n  mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n  // Merge stage\n  merge(nums, left, mid, right);\n}\n
    merge_sort.rs
    /* Merge left subarray and right subarray */\nfn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    let tmp_size = right - left + 1;\n    let mut tmp = vec![0; tmp_size];\n    // Initialize the start indices of the left and right subarrays\n    let (mut i, mut j, mut k) = (left, mid + 1, 0);\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\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    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in 0..tmp_size {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfn merge_sort(nums: &mut [i32], left: usize, right: usize) {\n    // Termination condition\n    if left >= right {\n        return; // Terminate recursion when subarray length is 1\n    }\n\n    // Divide and conquer stage\n    let mid = left + (right - left) / 2; // Calculate midpoint\n    merge_sort(nums, left, mid); // Recursively process the left subarray\n    merge_sort(nums, mid + 1, right); // Recursively process the right subarray\n\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.c
    /* Merge left subarray and right subarray */\nvoid merge(int *nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int tmpSize = right - left + 1;\n    int *tmp = (int *)malloc(tmpSize * sizeof(int));\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmpSize; ++k) {\n        nums[left + k] = tmp[k];\n    }\n    // Free memory\n    free(tmp);\n}\n\n/* Merge sort */\nvoid mergeSort(int *nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    mergeSort(nums, left, mid);      // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
    merge_sort.kt
    /* Merge left subarray and right subarray */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    val tmp = IntArray(right - left + 1)\n    // Initialize the start indices of the left and right subarrays\n    var i = left\n    var j = mid + 1\n    var k = 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\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    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++]\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++]\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (l in tmp.indices) {\n        nums[left + l] = tmp[l]\n    }\n}\n\n/* Merge sort */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n    // Termination condition\n    if (left >= right) return  // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    val mid = left + (right - left) / 2 // Calculate midpoint\n    mergeSort(nums, left, mid) // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right) // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right)\n}\n
    merge_sort.rb
    ### Merge left and right subarrays ###\ndef merge(nums, left, mid, right)\n  # Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n  # Create temporary array tmp to store merged result\n  tmp = Array.new(right - left + 1, 0)\n  # Initialize the start indices of the left and right subarrays\n  i, j, k = left, mid + 1, 0\n  # While both subarrays still have elements, compare and copy the smaller element into the temporary array\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  # Copy the remaining elements of the left and right subarrays into the temporary array\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  # Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n  (0...tmp.length).each do |k|\n    nums[left + k] = tmp[k]\n  end\nend\n\n### Merge sort ###\ndef merge_sort(nums, left, right)\n  # Termination condition\n  # Terminate recursion when subarray length is 1\n  return if left >= right\n  # Divide and conquer stage\n  mid = left + (right - left) / 2 # Calculate midpoint\n  merge_sort(nums, left, mid) # Recursively process the left subarray\n  merge_sort(nums, mid + 1, right) # Recursively process the right subarray\n  # Merge stage\n  merge(nums, left, mid, right)\nend\n
    ","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162-algorithm-characteristics","level":2,"title":"11.6.2   Algorithm Characteristics","text":"
    • Time complexity is \\(O(n \\log n)\\); merge sort is non-adaptive: The divide phase produces a recursion tree of height \\(\\log n\\), and the total number of operations performed during merging at each level is \\(n\\), so the overall time complexity is \\(O(n \\log n)\\).
    • Space complexity is \\(O(n)\\); merge sort is not in-place: The recursion depth is \\(\\log n\\), which uses \\(O(\\log n)\\) stack-frame space. The merge operation requires an auxiliary array, which uses \\(O(n)\\) additional space.
    • Stable sort: During merging, the relative order of equal elements remains unchanged.
    ","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163-linked-list-sorting","level":2,"title":"11.6.3   Linked List Sorting","text":"

    For linked lists, merge sort has significant advantages over other sorting algorithms, and it can reduce the space complexity of the sorting task to \\(O(1)\\).

    • Divide phase: Iteration can be used instead of recursion to split the linked list, thereby eliminating the stack-frame space used by recursion.
    • Merge phase: In linked lists, node insertion and deletion require only pointer updates, so the merge phase (merging two short sorted linked lists into one longer sorted linked list) does not require creating an additional linked list.

    The specific implementation details are quite complex, and interested readers can consult related materials for learning.

    ","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   Quick Sort","text":"

    Quick sort is an efficient and widely used sorting algorithm based on the divide-and-conquer strategy.

    The core operation of quick sort is \"sentinel partitioning\", whose goal is to select an element as the \"pivot\", move all elements smaller than the pivot to its left, and move all elements larger than the pivot to its right. Specifically, the process is shown in Figure 11-8.

    1. Select the leftmost element as the pivot, and initialize two pointers i and j at the two ends of the array.
    2. Enter a loop. In each round, use i (j) to find the first element larger (smaller) than the pivot, and then swap the two elements.
    3. Repeat step 2. until i and j meet, then swap the pivot into the boundary position between the two sub-arrays.
    <1><2><3><4><5><6><7><8><9>

    Figure 11-8   Sentinel partitioning steps

    After sentinel partitioning, the original array is divided into three parts: the left sub-array, the pivot, and the right sub-array, such that \"any element in the left sub-array \\(\\leq\\) the pivot \\(\\leq\\) any element in the right sub-array\". Therefore, we only need to sort the two sub-arrays next.

    Divide-and-conquer strategy of quick sort

    The essence of sentinel partitioning is to simplify the sorting problem of a longer array into the sorting problems of two shorter arrays.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Sentinel partition\"\"\"\n    # Use nums[left] as the pivot\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Search from right to left for the first element smaller than the pivot\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Search from left to right for the first element greater than the pivot\n        # Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    # Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Return the index of the pivot\n
    quick_sort.cpp
    /* Sentinel partition */\nint partition(vector<int> &nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Search from left to right for the first element greater than the pivot\n        swap(nums[i], nums[j]); // Swap these two elements\n    }\n    swap(nums[i], nums[left]);  // Swap the pivot to the boundary between the two subarrays\n    return i;                   // Return the index of the pivot\n}\n
    quick_sort.java
    /* Swap elements */\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/* Sentinel partition */\nint partition(int[] nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
    quick_sort.cs
    /* Swap elements */\nvoid Swap(int[] nums, int i, int j) {\n    (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* Sentinel partition */\nint Partition(int[] nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        Swap(nums, i, j); // Swap these two elements\n    }\n    Swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
    quick_sort.go
    /* Sentinel partition */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n    // Use nums[left] as the pivot\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Search from right to left for the first element smaller than the pivot\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Return the index of the pivot\n}\n
    quick_sort.swift
    /* Sentinel partition */\nfunc partition(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while i < j {\n        while i < j, nums[j] >= nums[left] {\n            j -= 1 // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j, nums[i] <= nums[left] {\n            i += 1 // Search from left to right for the first element greater than the pivot\n        }\n        nums.swapAt(i, j) // Swap these two elements\n    }\n    nums.swapAt(i, left) // Swap the pivot to the boundary between the two subarrays\n    return i // Return the index of the pivot\n}\n
    quick_sort.js
    /* Swap elements */\nswap(nums, i, j) {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\npartition(nums, left, right) {\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
    quick_sort.ts
    /* Swap elements */\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/* Sentinel partition */\npartition(nums: number[], left: number, right: number): number {\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
    quick_sort.dart
    /* Swap elements */\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/* Sentinel partition */\nint _partition(List<int> nums, int left, int right) {\n  // Use nums[left] as the pivot\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n    while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n    _swap(nums, i, j); // Swap these two elements\n  }\n  _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n  return i; // Return the index of the pivot\n}\n
    quick_sort.rs
    /* Sentinel partition */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Use nums[left] as the pivot\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        nums.swap(i, j); // Swap these two elements\n    }\n    nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays\n    i // Return the index of the pivot\n}\n
    quick_sort.c
    /* Swap elements */\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/* Sentinel partition */\nint partition(int nums[], int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap these two elements\n        swap(nums, i, j);\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    swap(nums, i, left);\n    // Return the index of the pivot\n    return i;\n}\n
    quick_sort.kt
    /* Swap elements */\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/* Sentinel partition */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--           // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++           // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j)  // Swap these two elements\n    }\n    swap(nums, i, left)   // Swap the pivot to the boundary between the two subarrays\n    return i              // Return the index of the pivot\n}\n
    quick_sort.rb
    ### Sentinel partition ###\ndef partition(nums, left, right)\n  # Use nums[left] as the pivot\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Search from right to left for the first element smaller than the pivot\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Search from left to right for the first element greater than the pivot\n    end\n    # Swap elements\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Swap the pivot to the boundary between the two subarrays\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Return the index of the pivot\nend\n
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151-algorithm-flow","level":2,"title":"11.5.1   Algorithm Flow","text":"

    The overall flow of quick sort is shown in Figure 11-9.

    1. First, perform one \"sentinel partitioning\" on the original array to obtain the unsorted left sub-array and right sub-array.
    2. Then, recursively perform \"sentinel partitioning\" on the left sub-array and right sub-array respectively.
    3. Continue recursively until the sub-array length is 1, at which point sorting of the entire array is complete.

    Figure 11-9   Quick sort flow

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Quick sort\"\"\"\n    # Terminate recursion when subarray length is 1\n    if left >= right:\n        return\n    # Sentinel partition\n    pivot = self.partition(nums, left, right)\n    # Recursively process the left subarray and right subarray\n    self.quick_sort(nums, left, pivot - 1)\n    self.quick_sort(nums, pivot + 1, right)\n
    quick_sort.cpp
    /* Quick sort */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.java
    /* Quick sort */\nvoid quickSort(int[] nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.cs
    /* Quick sort */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = Partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    QuickSort(nums, left, pivot - 1);\n    QuickSort(nums, pivot + 1, right);\n}\n
    quick_sort.go
    /* Quick sort */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return\n    }\n    // Sentinel partition\n    pivot := q.partition(nums, left, right)\n    // Recursively process the left subarray and right subarray\n    q.quickSort(nums, left, pivot-1)\n    q.quickSort(nums, pivot+1, right)\n}\n
    quick_sort.swift
    /* Quick sort */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return\n    }\n    // Sentinel partition\n    let pivot = partition(nums: &nums, left: left, right: right)\n    // Recursively process the left subarray and right subarray\n    quickSort(nums: &nums, left: left, right: pivot - 1)\n    quickSort(nums: &nums, left: pivot + 1, right: right)\n}\n
    quick_sort.js
    /* Quick sort */\nquickSort(nums, left, right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) return;\n    // Sentinel partition\n    const pivot = this.partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.ts
    /* Quick sort */\nquickSort(nums: number[], left: number, right: number): void {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) {\n        return;\n    }\n    // Sentinel partition\n    const pivot = this.partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.dart
    /* Quick sort */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Terminate recursion when subarray length is 1\n  if (left >= right) return;\n  // Sentinel partition\n  int pivot = _partition(nums, left, right);\n  // Recursively process the left subarray and right subarray\n  quickSort(nums, left, pivot - 1);\n  quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.rs
    /* Quick sort */\npub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return;\n    }\n    // Sentinel partition\n    let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n    // Recursively process the left subarray and right subarray\n    Self::quick_sort(left, pivot - 1, nums);\n    Self::quick_sort(pivot + 1, right, nums);\n}\n
    quick_sort.c
    /* Quick sort */\nvoid quickSort(int nums[], int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) {\n        return;\n    }\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
    quick_sort.kt
    /* Quick sort */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) return\n    // Sentinel partition\n    val pivot = partition(nums, left, right)\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1)\n    quickSort(nums, pivot + 1, right)\n}\n
    quick_sort.rb
    ### Quick sort class ###\ndef quick_sort(nums, left, right)\n  # Recurse when subarray length is not 1\n  if left < right\n    # Sentinel partition\n    pivot = partition(nums, left, right)\n    # Recursively process the left subarray and right subarray\n    quick_sort(nums, left, pivot - 1)\n    quick_sort(nums, pivot + 1, right)\n  end\n  nums\nend\n
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152-algorithm-characteristics","level":2,"title":"11.5.2   Algorithm Characteristics","text":"
    • Time complexity of \\(O(n \\log n)\\), non-adaptive sorting: On average, sentinel partitioning produces \\(\\log n\\) recursive levels, and the total number of loop iterations across each level is \\(n\\), so the overall time complexity is \\(O(n \\log n)\\). In the worst case, each round of sentinel partitioning splits an array of length \\(n\\) into sub-arrays of lengths \\(0\\) and \\(n - 1\\). The recursion depth then reaches \\(n\\), with \\(n\\) loop iterations at each level, yielding an overall time complexity of \\(O(n^2)\\).
    • Space complexity of \\(O(n)\\), in-place sorting: In the case where the input array is completely reversed, the worst recursive depth reaches \\(n\\), using \\(O(n)\\) stack frame space. The sorting operation is performed on the original array without the aid of an additional array.
    • Unstable sorting: In the last step of sentinel partitioning, the pivot may be swapped to the right of an equal element.
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153-why-is-quick-sort-fast","level":2,"title":"11.5.3   Why Is Quick Sort Fast","text":"

    As the name suggests, quick sort has clear efficiency advantages. Although its average time complexity is the same as that of \"merge sort\" and \"heap sort\", quick sort is usually faster in practice for the following reasons.

    • The worst case is unlikely to occur: Although the worst-case time complexity of quick sort is \\(O(n^2)\\) and its performance is less predictable than that of merge sort, quick sort runs in \\(O(n \\log n)\\) time in the vast majority of cases.
    • High cache efficiency: During sentinel partitioning, the system can load the entire sub-array into cache, so accessing elements is relatively efficient. By contrast, algorithms such as \"heap sort\" require non-contiguous access to elements and therefore do not enjoy this advantage.
    • Small constant factors: Among the three algorithms above, quick sort performs the fewest comparisons, assignments, and swaps in total. This is similar to why \"insertion sort\" is faster than \"bubble sort\".
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154-pivot-optimization","level":2,"title":"11.5.4   Pivot Optimization","text":"

    Quick sort can become less time-efficient for certain inputs. Consider an extreme example in which the input array is in completely descending order. Because we choose the leftmost element as the pivot, once sentinel partitioning is complete, the pivot is swapped to the far right of the array, leaving a left sub-array of length \\(n - 1\\) and a right sub-array of length \\(0\\). If this continues recursively, each round of sentinel partitioning produces one sub-array of length \\(0\\), the divide-and-conquer strategy breaks down, and quick sort degenerates into an approximation of \"bubble sort\".

    To reduce the chance of this happening, we can optimize the pivot selection strategy used in sentinel partitioning. For example, we can choose a pivot at random. However, if we are unlucky and repeatedly pick poor pivots, performance can still be unsatisfactory.

    It should be noted that programming languages usually generate \"pseudo-random numbers\". If we construct a specific test case against a pseudo-random sequence, quick sort can still suffer degraded performance.

    To improve further, we can choose three candidate elements from the array, usually the first, last, and middle elements, and use the median of the three as the pivot. This greatly increases the chance that the pivot is \"neither too small nor too large\". We can also choose more candidate elements to further improve the robustness of the algorithm. With this method, the probability that the time complexity degrades to \\(O(n^2)\\) is significantly reduced.

    Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"Select the median of three candidate elements\"\"\"\n    l, m, r = nums[left], nums[mid], nums[right]\n    if (l <= m <= r) or (r <= m <= l):\n        return mid  # m is between l and r\n    if (m <= l <= r) or (r <= l <= m):\n        return left  # l is between m and r\n    return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Sentinel partition (median of three)\"\"\"\n    # Use nums[left] as the pivot\n    med = self.median_three(nums, left, (left + right) // 2, right)\n    # Swap the median to the array's leftmost position\n    nums[left], nums[med] = nums[med], nums[left]\n    # Use nums[left] as the pivot\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Search from right to left for the first element smaller than the pivot\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Search from left to right for the first element greater than the pivot\n        # Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    # Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Return the index of the pivot\n
    quick_sort.cpp
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partition(vector<int> &nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums[left], nums[med]);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Search from left to right for the first element greater than the pivot\n        swap(nums[i], nums[j]); // Swap these two elements\n    }\n    swap(nums[i], nums[left]);  // Swap the pivot to the boundary between the two subarrays\n    return i;                   // Return the index of the pivot\n}\n
    quick_sort.java
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partition(int[] nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
    quick_sort.cs
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint Partition(int[] nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = MedianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    Swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        Swap(nums, i, j); // Swap these two elements\n    }\n    Swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
    quick_sort.go
    /* Select the median of three candidate elements */\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 is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l is between m and r\n    }\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n    // Use nums[left] as the pivot\n    med := q.medianThree(nums, left, (left+right)/2, right)\n    // Swap the median to the array's leftmost position\n    nums[left], nums[med] = nums[med], nums[left]\n    // Use nums[left] as the pivot\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Search from right to left for the first element smaller than the pivot\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Return the index of the pivot\n}\n
    quick_sort.swift
    /* Select the median of three candidate elements */\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 is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l is between m and r\n    }\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Select the median of three candidate elements\n    let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n    // Swap the median to the array's leftmost position\n    nums.swapAt(left, med)\n    return partition(nums: &nums, left: left, right: right)\n}\n
    quick_sort.js
    /* Select the median of three candidate elements */\nmedianThree(nums, left, mid, right) {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m is between l and r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l is between m and r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Sentinel partition (median of three) */\npartition(nums, left, right) {\n    // Select the median of three candidate elements\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Swap the median to the array's leftmost position\n    this.swap(nums, left, med);\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
    quick_sort.ts
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l is between m and r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Sentinel partition (median of three) */\npartition(nums: number[], left: number, right: number): number {\n    // Select the median of three candidate elements\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Swap the median to the array's leftmost position\n    this.swap(nums, left, med);\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Search from left to right for the first element greater than the pivot\n        }\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
    quick_sort.dart
    /* Select the median of three candidate elements */\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 is between l and r\n  if ((m <= l && l <= r) || (r <= l && l <= m))\n    return left; // l is between m and r\n  return right;\n}\n\n/* Sentinel partition (median of three) */\nint _partition(List<int> nums, int left, int right) {\n  // Select the median of three candidate elements\n  int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n  // Swap the median to the array's leftmost position\n  _swap(nums, left, med);\n  // Use nums[left] as the pivot\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n    while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n    _swap(nums, i, j); // Swap these two elements\n  }\n  _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n  return i; // Return the index of the pivot\n}\n
    quick_sort.rs
    /* Select the median of three candidate elements */\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 is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left; // l is between m and r\n    }\n    right\n}\n\n/* Sentinel partition (median of three) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Select the median of three candidate elements\n    let med = Self::median_three(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    nums.swap(left, med);\n    // Use nums[left] as the pivot\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        nums.swap(i, j); // Swap these two elements\n    }\n    nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays\n    i // Return the index of the pivot\n}\n
    quick_sort.c
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partitionMedian(int nums[], int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--; // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i;            // Return the index of the pivot\n}\n
    quick_sort.kt
    /* Select the median of three candidate elements */\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 is between l and r\n    if ((l in m..r) || (l in r..m))\n        return left // l is between m and r\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // Select the median of three candidate elements\n    val med = medianThree(nums, left, (left + right) / 2, right)\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med)\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--                      // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++                      // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j)             // Swap these two elements\n    }\n    swap(nums, i, left)              // Swap the pivot to the boundary between the two subarrays\n    return i                         // Return the index of the pivot\n}\n
    quick_sort.rb
    ### Select median of three candidate elements ###\ndef median_three(nums, left, mid, right)\n  # Select the median of three candidate elements\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m is between l and r\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l is between m and r\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n### Sentinel partition (median of three) ###\ndef partition(nums, left, right)\n  ### Use nums[left] as pivot\n  med = median_three(nums, left, (left + right) / 2, right)\n  # Swap median to leftmost position of array\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 # Search from right to left for the first element smaller than the pivot\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Search from left to right for the first element greater than the pivot\n    end\n    # Swap elements\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Swap the pivot to the boundary between the two subarrays\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Return the index of the pivot\nend\n
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155-recursive-depth-optimization","level":2,"title":"11.5.5   Recursive Depth Optimization","text":"

    Quick sort may also use more space for certain inputs. Consider a fully sorted input array. Let the length of the current sub-array in the recursion be \\(m\\). Each round of sentinel partitioning produces a left sub-array of length \\(0\\) and a right sub-array of length \\(m - 1\\), which means each recursive call reduces the problem size by only one element. The recursion tree can therefore reach a height of \\(n - 1\\), requiring \\(O(n)\\) stack-frame space.

    To prevent stack frames from accumulating, we can compare the lengths of the two sub-arrays after each round of sentinel partitioning, and recurse only on the shorter one. Because the shorter sub-array has length at most \\(n / 2\\), this method ensures that the recursion depth does not exceed \\(\\log n\\), reducing the worst-case space complexity to \\(O(\\log n)\\). The code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Quick sort (recursion depth optimization)\"\"\"\n    # Terminate when subarray length is 1\n    while left < right:\n        # Sentinel partition operation\n        pivot = self.partition(nums, left, right)\n        # Perform quick sort on the shorter of the two subarrays\n        if pivot - left < right - pivot:\n            self.quick_sort(nums, left, pivot - 1)  # Recursively sort the left subarray\n            left = pivot + 1  # Remaining unsorted interval is [pivot + 1, right]\n        else:\n            self.quick_sort(nums, pivot + 1, right)  # Recursively sort the right subarray\n            right = pivot - 1  # Remaining unsorted interval is [left, pivot - 1]\n
    quick_sort.cpp
    /* Quick sort (recursion depth optimization) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1;                 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1;                 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.java
    /* Quick sort (recursion depth optimization) */\nvoid quickSort(int[] nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.cs
    /* Quick sort (recursion depth optimization) */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = Partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            QuickSort(nums, left, pivot - 1);  // Recursively sort the left subarray\n            left = pivot + 1;  // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            QuickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.go
    /* Quick sort (recursion depth optimization) */\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n    // Terminate when subarray length is 1\n    for left < right {\n        // Sentinel partition operation\n        pivot := q.partition(nums, left, right)\n        // Perform quick sort on the shorter of the two subarrays\n        if pivot-left < right-pivot {\n            q.quickSort(nums, left, pivot-1) // Recursively sort the left subarray\n            left = pivot + 1                 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            q.quickSort(nums, pivot+1, right) // Recursively sort the right subarray\n            right = pivot - 1                 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.swift
    /* Quick sort (recursion depth optimization) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n    var left = left\n    var right = right\n    // Terminate when subarray length is 1\n    while left < right {\n        // Sentinel partition operation\n        let pivot = partition(nums: &nums, left: left, right: right)\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left) < (right - pivot) {\n            quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // Recursively sort the left subarray\n            left = pivot + 1 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // Recursively sort the right subarray\n            right = pivot - 1 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.js
    /* Quick sort (recursion depth optimization) */\nquickSort(nums, left, right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        let pivot = this.partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.ts
    /* Quick sort (recursion depth optimization) */\nquickSort(nums: number[], left: number, right: number): void {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        let pivot = this.partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.dart
    /* Quick sort (recursion depth optimization) */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Terminate when subarray length is 1\n  while (left < right) {\n    // Sentinel partition operation\n    int pivot = _partition(nums, left, right);\n    // Perform quick sort on the shorter of the two subarrays\n    if (pivot - left < right - pivot) {\n      quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n      left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n    } else {\n      quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n      right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n    }\n  }\n}\n
    quick_sort.rs
    /* Quick sort (recursion depth optimization) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {\n    // Terminate when subarray length is 1\n    while left < right {\n        // Sentinel partition operation\n        let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n        // Perform quick sort on the shorter of the two subarrays\n        if pivot - left < right - pivot {\n            Self::quick_sort(left, pivot - 1, nums); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            Self::quick_sort(pivot + 1, right, nums); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.c
    /* Quick sort (recursion depth optimization) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            // Recursively sort the left subarray\n            quickSortTailCall(nums, left, pivot - 1);\n            // Remaining unsorted interval is [pivot + 1, right]\n            left = pivot + 1;\n        } else {\n            // Recursively sort the right subarray\n            quickSortTailCall(nums, pivot + 1, right);\n            // Remaining unsorted interval is [left, pivot - 1]\n            right = pivot - 1;\n        }\n    }\n}\n
    quick_sort.kt
    /* Quick sort (recursion depth optimization) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n    // Terminate when subarray length is 1\n    var l = left\n    var r = right\n    while (l < r) {\n        // Sentinel partition operation\n        val pivot = partition(nums, l, r)\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - l < r - pivot) {\n            quickSort(nums, l, pivot - 1) // Recursively sort the left subarray\n            l = pivot + 1 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, r) // Recursively sort the right subarray\n            r = pivot - 1 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
    quick_sort.rb
    ### Quick sort (recursion depth optimization) ###\ndef quick_sort(nums, left, right)\n  # Recurse when subarray length is not 1\n  while left < right\n    # Sentinel partition\n    pivot = partition(nums, left, right)\n    # Perform quick sort on the shorter of the two subarrays\n    if pivot - left < right - pivot\n      quick_sort(nums, left, pivot - 1)\n      left = pivot + 1 # Remaining unsorted interval is [pivot + 1, right]\n    else\n      quick_sort(nums, pivot + 1, right)\n      right = pivot - 1 # Remaining unsorted interval is [left, pivot - 1]\n    end\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   Radix Sort","text":"

    The previous section introduced counting sort, which is suitable when the number of items \\(n\\) is large but the value range \\(m\\) is small. Suppose we need to sort \\(n = 10^6\\) student IDs, each of which is an 8-digit number. Then the value range \\(m = 10^8\\) is very large. Using counting sort would require a large amount of memory, whereas radix sort avoids this problem.

    Radix sort is based on the same core idea as counting sort: it also sorts by counting occurrences. Building on this, radix sort exploits the positional relationship among digits and sorts them one digit at a time to obtain the final result.

    ","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101-algorithm-flow","level":2,"title":"11.10.1   Algorithm Flow","text":"

    Taking student ID data as an example, assume the lowest digit is the \\(1\\)st digit and the highest digit is the \\(8\\)th digit. The flow of radix sort is shown in Figure 11-18.

    1. Initialize the digit \\(k = 1\\).
    2. Perform \"counting sort\" on the \\(k\\)th digit of the student IDs. After completion, the data will be sorted from smallest to largest according to the \\(k\\)th digit.
    3. Increase \\(k\\) by \\(1\\), then return to step 2. and continue iterating until all digits are sorted, at which point the process ends.

    Figure 11-18   Radix sort algorithm flow

    Next, let us look at the code. For a number \\(x\\) in base \\(d\\), its \\(k\\)th digit \\(x_k\\) can be obtained with the following formula:

    \\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\]

    Here, \\(\\lfloor a \\rfloor\\) denotes rounding the floating-point number \\(a\\) down, and \\(\\bmod \\: d\\) denotes taking the remainder modulo \\(d\\). For student ID data, \\(d = 10\\) and \\(k \\in [1, 8]\\).

    Additionally, we need to slightly modify the counting sort code to make it sort based on the \\(k\\)th digit of the number:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py
    def digit(num: int, exp: int) -> int:\n    \"\"\"Get the k-th digit of element num, where exp = 10^(k-1)\"\"\"\n    # Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"Counting sort (based on nums k-th digit)\"\"\"\n    # Decimal digit range is 0~9, therefore need a bucket array of length 10\n    counter = [0] * 10\n    n = len(nums)\n    # Count the occurrence of digits 0~9\n    for i in range(n):\n        d = digit(nums[i], exp)  # Get the k-th digit of nums[i], noted as d\n        counter[d] += 1  # Count the occurrence of digit d\n    # Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in range(1, 10):\n        counter[i] += counter[i - 1]\n    # Traverse in reverse, based on bucket statistics, place each element into 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  # Get the index j for d in the array\n        res[j] = nums[i]  # Place the current element at index j\n        counter[d] -= 1  # Decrease the count of d by 1\n    # Use result to overwrite the original array nums\n    for i in range(n):\n        nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n    \"\"\"Radix sort\"\"\"\n    # Get the maximum element of the array, used to determine the maximum number of digits\n    m = max(nums)\n    # Traverse from the lowest to the highest digit\n    exp = 1\n    while exp <= m:\n        # Perform counting sort on the k-th digit of array elements\n        # k = 1 -> exp = 1\n        # k = 2 -> exp = 10\n        # i.e., exp = 10^(k-1)\n        counting_sort_digit(nums, exp)\n        exp *= 10\n
    radix_sort.cpp
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    vector<int> counter(10, 0);\n    int n = nums.size();\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(vector<int> &nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = *max_element(nums.begin(), nums.end());\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10)\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n}\n
    radix_sort.java
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int[] counter = new int[10];\n    int n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(int[] nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = Integer.MIN_VALUE;\n    for (int num : nums)\n        if (num > m)\n            m = num;\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
    radix_sort.cs
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int[] counter = new int[10];\n    int n = nums.Length;\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = Digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nvoid RadixSort(int[] nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = int.MinValue;\n    foreach (int num in nums) {\n        if (num > m) m = num;\n    }\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        CountingSortDigit(nums, exp);\n    }\n}\n
    radix_sort.go
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunc countingSortDigit(nums []int, exp int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    counter := make([]int, 10)\n    n := len(nums)\n    // Count the occurrence of digits 0~9\n    for i := 0; i < n; i++ {\n        d := digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d\n        counter[d]++             // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i := 1; i < 10; i++ {\n        counter[i] += counter[i-1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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 // Get the index j for d in the array\n        res[j] = nums[i]    // Place the current element at index j\n        counter[d]--        // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for i := 0; i < n; i++ {\n        nums[i] = res[i]\n    }\n}\n\n/* Radix sort */\nfunc radixSort(nums []int) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    max := math.MinInt\n    for _, num := range nums {\n        if num > max {\n            max = num\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for exp := 1; max >= exp; exp *= 10 {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n    }\n}\n
    radix_sort.swift
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    var counter = Array(repeating: 0, count: 10)\n    // Count the occurrence of digits 0~9\n    for i in nums.indices {\n        let d = digit(num: nums[i], exp: exp) // Get the k-th digit of nums[i], noted as d\n        counter[d] += 1 // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in 1 ..< 10 {\n        counter[i] += counter[i - 1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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 // Get the index j for d in the array\n        res[j] = nums[i] // Place the current element at index j\n        counter[d] -= 1 // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n\n/* Radix sort */\nfunc radixSort(nums: inout [Int]) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    var m = Int.min\n    for num in nums {\n        if num > m {\n            m = num\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums: &nums, exp: exp)\n    }\n}\n
    radix_sort.js
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return Math.floor(num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunction countingSortDigit(nums, exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d]--; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nfunction radixSort(nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m = Math.max(... nums);\n    // Traverse from the lowest to the highest digit\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
    radix_sort.ts
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return Math.floor(num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d]--; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nfunction radixSort(nums: number[]): void {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m: number = Math.max(... nums);\n    // Traverse from the lowest to the highest digit\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
    radix_sort.dart
    /* Get k-th digit of element _num, where exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // Passing exp instead of k can avoid repeated expensive exponentiation here\n  return (_num ~/ exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // Decimal digit range is 0~9, therefore need a bucket array of length 10\n  List<int> counter = List<int>.filled(10, 0);\n  int n = nums.length;\n  // Count the occurrence of digits 0~9\n  for (int i = 0; i < n; i++) {\n    int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n    counter[d]++; // Count the occurrence of digit d\n  }\n  // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n  for (int i = 1; i < 10; i++) {\n    counter[i] += counter[i - 1];\n  }\n  // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n    res[j] = nums[i]; // Place the current element at index j\n    counter[d]--; // Decrease the count of d by 1\n  }\n  // Use result to overwrite the original array nums\n  for (int i = 0; i < n; i++) nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(List<int> nums) {\n  // Get the maximum element of the array, used to determine the maximum number of digits\n  // In Dart, int length is 64 bits\n  int m = -1 << 63;\n  for (int _num in nums) if (_num > m) m = _num;\n  // Traverse from the lowest to the highest digit\n  for (int exp = 1; exp <= m; exp *= 10)\n    // Perform counting sort on the k-th digit of array elements\n    // k = 1 -> exp = 1\n    // k = 2 -> exp = 10\n    // i.e., exp = 10^(k-1)\n    countingSortDigit(nums, exp);\n}\n
    radix_sort.rs
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return ((num / exp) % 10) as usize;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    let mut counter = [0; 10];\n    let n = nums.len();\n    // Count the occurrence of digits 0~9\n    for i in 0..n {\n        let d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d] += 1; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in 1..10 {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d] -= 1; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    nums.copy_from_slice(&res);\n}\n\n/* Radix sort */\nfn radix_sort(nums: &mut [i32]) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m = *nums.into_iter().max().unwrap();\n    // Traverse from the lowest to the highest digit\n    let mut exp = 1;\n    while exp <= m {\n        counting_sort_digit(nums, exp);\n        exp *= 10;\n    }\n}\n
    radix_sort.c
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // Initialize to 0 to support subsequent memory release\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < size; i++) {\n        // Get the k-th digit of nums[i], noted as d\n        int d = digit(nums[i], exp);\n        // Count the occurrence of digit d\n        counter[d]++;\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < size; i++) {\n        nums[i] = res[i];\n    }\n    // Free memory\n    free(res);\n    free(counter);\n}\n\n/* Radix sort */\nvoid radixSort(int nums[], int size) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\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    // Traverse from the lowest to the highest digit\n    for (int exp = 1; max >= exp; exp *= 10)\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, size, exp);\n}\n
    radix_sort.kt
    /* Get the k-th digit of element num, where exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    val counter = IntArray(10)\n    val n = nums.size\n    // Count the occurrence of digits 0~9\n    for (i in 0..<n) {\n        val d = digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d\n        counter[d]++                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (i in 1..9) {\n        counter[i] += counter[i - 1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into 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 // Get the index j for d in the array\n        res[j] = nums[i]       // Place the current element at index j\n        counter[d]--           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (i in 0..<n)\n        nums[i] = res[i]\n}\n\n/* Radix sort */\nfun radixSort(nums: IntArray) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    var m = Int.MIN_VALUE\n    for (num in nums) if (num > m) m = num\n    var exp = 1\n    // Traverse from the lowest to the highest digit\n    while (exp <= m) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n        exp *= 10\n    }\n}\n
    radix_sort.rb
    ### Get k-th digit of element num, where exp = 10^(k-1) ###\ndef digit(num, exp)\n  # Passing exp instead of k avoids expensive exponentiation calculations\n  (num / exp) % 10\nend\n\n### Counting sort (sort by k-th digit of nums) ###\ndef counting_sort_digit(nums, exp)\n  # Decimal digit range is 0~9, therefore need a bucket array of length 10\n  counter = Array.new(10, 0)\n  n = nums.length\n  # Count the occurrence of digits 0~9\n  for i in 0...n\n    d = digit(nums[i], exp) # Get the k-th digit of nums[i], noted as d\n    counter[d] += 1 # Count the occurrence of digit d\n  end\n  # Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n  (1...10).each { |i| counter[i] += counter[i - 1] }\n  # Traverse in reverse, based on bucket statistics, place each element into 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 # Get the index j for d in the array\n    res[j] = nums[i] # Place the current element at index j\n    counter[d] -= 1 # Decrease the count of d by 1\n  end\n  # Use result to overwrite the original array nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n\n### Radix sort ###\ndef radix_sort(nums)\n  # Get the maximum element of the array, used to determine the maximum number of digits\n  m = nums.max\n  # Traverse from the lowest to the highest digit\n  exp = 1\n  while exp <= m\n    # Perform counting sort on the k-th digit of array elements\n    # k = 1 -> exp = 1\n    # k = 2 -> exp = 10\n    # i.e., exp = 10^(k-1)\n    counting_sort_digit(nums, exp)\n    exp *= 10\n  end\nend\n

    Why start sorting from the lowest digit?

    In successive sorting passes, a later pass overrides the result of an earlier one. For example, if the first pass yields \\(a < b\\) but the second yields \\(a > b\\), then the result of the second pass prevails. Because higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then the higher digits.

    ","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102-algorithm-characteristics","level":2,"title":"11.10.2   Algorithm Characteristics","text":"

    Compared with counting sort, radix sort is suitable for larger value ranges, but only when the data can be represented with a fixed number of digits and that digit count is not too large. For example, floating-point numbers are not well suited to radix sort because the digit count \\(k\\) can be too large, potentially leading to time complexity \\(O(nk) \\gg O(n^2)\\).

    • Time complexity of \\(O(nk)\\), non-adaptive sorting: Let the number of items be \\(n\\), let the values be represented in base \\(d\\), and let the maximum number of digits be \\(k\\). Counting sort on one digit takes \\(O(n + d)\\) time, so sorting all \\(k\\) digits takes \\(O((n + d)k)\\) time. In practice, \\(d\\) and \\(k\\) are usually relatively small, so the overall time complexity approaches \\(O(n)\\).
    • Space complexity of \\(O(n + d)\\), non-in-place sorting: Same as counting sort, radix sort requires auxiliary arrays res and counter of lengths \\(n\\) and \\(d\\).
    • Stable sort: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee correct sorting results.
    ","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   Selection Sort","text":"

    Selection sort works very simply: in each round, it selects the smallest element from the unsorted interval and places it at the end of the sorted interval.

    Assume the array has length \\(n\\). The procedure of selection sort is shown in Figure 11-2.

    1. Initially, all elements are unsorted, i.e., the unsorted (index) interval is \\([0, n-1]\\).
    2. Select the smallest element in the interval \\([0, n-1]\\) and swap it with the element at index \\(0\\). After completion, the first element of the array is sorted.
    3. Select the smallest element in the interval \\([1, n-1]\\) and swap it with the element at index \\(1\\). After completion, the first 2 elements of the array are sorted.
    4. And so on. After \\(n - 1\\) rounds of selection and swapping, the first \\(n - 1\\) elements of the array are sorted.
    5. The only remaining element must be the largest, so no further sorting is needed and the array is sorted.
    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 11-2   Selection sort steps

    In the code, we use \\(k\\) to track the smallest element within the unsorted interval:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py
    def selection_sort(nums: list[int]):\n    \"\"\"Selection sort\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [i, n-1]\n    for i in range(n - 1):\n        # Inner loop: find the smallest element within the unsorted interval\n        k = i\n        for j in range(i + 1, n):\n            if nums[j] < nums[k]:\n                k = j  # Record the index of the smallest element\n        # Swap the smallest element with the first element of the unsorted interval\n        nums[i], nums[k] = nums[k], nums[i]\n
    selection_sort.cpp
    /* Selection sort */\nvoid selectionSort(vector<int> &nums) {\n    int n = nums.size();\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        swap(nums[i], nums[k]);\n    }\n}\n
    selection_sort.java
    /* Selection sort */\nvoid selectionSort(int[] nums) {\n    int n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
    selection_sort.cs
    /* Selection sort */\nvoid SelectionSort(int[] nums) {\n    int n = nums.Length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        (nums[k], nums[i]) = (nums[i], nums[k]);\n    }\n}\n
    selection_sort.go
    /* Selection sort */\nfunc selectionSort(nums []int) {\n    n := len(nums)\n    // Outer loop: unsorted interval is [i, n-1]\n    for i := 0; i < n-1; i++ {\n        // Inner loop: find the smallest element within the unsorted interval\n        k := i\n        for j := i + 1; j < n; j++ {\n            if nums[j] < nums[k] {\n                // Record the index of the smallest element\n                k = j\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums[i], nums[k] = nums[k], nums[i]\n\n    }\n}\n
    selection_sort.swift
    /* Selection sort */\nfunc selectionSort(nums: inout [Int]) {\n    // Outer loop: unsorted interval is [i, n-1]\n    for i in nums.indices.dropLast() {\n        // Inner loop: find the smallest element within the unsorted interval\n        var k = i\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[j] < nums[k] {\n                k = j // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums.swapAt(i, k)\n    }\n}\n
    selection_sort.js
    /* Selection sort */\nfunction selectionSort(nums) {\n    let n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
    selection_sort.ts
    /* Selection sort */\nfunction selectionSort(nums: number[]): void {\n    let n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
    selection_sort.dart
    /* Selection sort */\nvoid selectionSort(List<int> nums) {\n  int n = nums.length;\n  // Outer loop: unsorted interval is [i, n-1]\n  for (int i = 0; i < n - 1; i++) {\n    // Inner loop: find the smallest element within the unsorted interval\n    int k = i;\n    for (int j = i + 1; j < n; j++) {\n      if (nums[j] < nums[k]) k = j; // Record the index of the smallest element\n    }\n    // Swap the smallest element with the first element of the unsorted interval\n    int temp = nums[i];\n    nums[i] = nums[k];\n    nums[k] = temp;\n  }\n}\n
    selection_sort.rs
    /* Selection sort */\nfn selection_sort(nums: &mut [i32]) {\n    if nums.is_empty() {\n        return;\n    }\n    let n = nums.len();\n    // Outer loop: unsorted interval is [i, n-1]\n    for i in 0..n - 1 {\n        // Inner loop: find the smallest element within the unsorted interval\n        let mut k = i;\n        for j in i + 1..n {\n            if nums[j] < nums[k] {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums.swap(i, k);\n    }\n}\n
    selection_sort.c
    /* Selection sort */\nvoid selectionSort(int nums[], int n) {\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
    selection_sort.kt
    /* Selection sort */\nfun selectionSort(nums: IntArray) {\n    val n = nums.size\n    // Outer loop: unsorted interval is [i, n-1]\n    for (i in 0..<n - 1) {\n        var k = i\n        // Inner loop: find the smallest element within the unsorted interval\n        for (j in i + 1..<n) {\n            if (nums[j] < nums[k])\n                k = j // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        val temp = nums[i]\n        nums[i] = nums[k]\n        nums[k] = temp\n    }\n}\n
    selection_sort.rb
    ### Selection sort ###\ndef selection_sort(nums)\n  n = nums.length\n  # Outer loop: unsorted interval is [i, n-1]\n  for i in 0...(n - 1)\n    # Inner loop: find the smallest element within the unsorted interval\n    k = i\n    for j in (i + 1)...n\n      if nums[j] < nums[k]\n        k = j # Record the index of the smallest element\n      end\n    end\n    # Swap the smallest element with the first element of the unsorted interval\n    nums[i], nums[k] = nums[k], nums[i]\n  end\nend\n
    ","path":["Chapter 11. Sorting","11.2   Selection Sort"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121-algorithm-characteristics","level":2,"title":"11.2.1   Algorithm Characteristics","text":"
    • Time complexity \\(O(n^2)\\), non-adaptive sorting: The outer loop has \\(n - 1\\) rounds in total. The length of the unsorted interval in the first round is \\(n\\), and the length of the unsorted interval in the last round is \\(2\\). That is, the rounds of the outer loop contain inner loops with \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(3\\), and \\(2\\) iterations, summing to \\(\\frac{(n - 1)(n + 2)}{2}\\).
    • Space complexity \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
    • Unstable sorting: As shown in Figure 11-3, element nums[i] may be swapped to the right of an element equal to it, causing a change in their relative order.

    Figure 11-3   Selection sort non-stability example

    ","path":["Chapter 11. Sorting","11.2   Selection Sort"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   Sorting Algorithm","text":"

    A sorting algorithm arranges a set of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently.

    As shown in Figure 11-1, the data being sorted can be integers, floating-point numbers, characters, strings, and so on. The sorting rule can be defined as needed, such as numerical order, ASCII order, or a custom rule.

    Figure 11-1   Data type and criterion examples

    ","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111-evaluation-dimensions","level":2,"title":"11.1.1   Evaluation Dimensions","text":"

    Execution efficiency: We expect the time complexity of sorting algorithms to be as low as possible, with a smaller total number of operations (reducing the constant factor in time complexity). For large data volumes, execution efficiency is particularly important.

    In-place property: As the name implies, in-place sorting achieves sorting by operating directly on the original array without requiring additional auxiliary arrays, thus saving memory. Typically, in-place sorting involves fewer data movement operations and runs faster.

    Stability: Stable sorting ensures that the relative order of equal elements in the array does not change after sorting is completed.

    Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, unstable sorting may cause the ordered nature of the input data to be lost:

    # The input data is sorted by name\n# (name, age)\n  ('A', 19)\n  ('B', 18)\n  ('C', 21)\n  ('D', 19)\n  ('E', 23)\n\n# Suppose we use an unstable sorting algorithm to sort the list by age.\n# In the result, the relative positions of ('D', 19) and ('A', 19) change,\n# so the property that the input data is sorted by name is lost.\n  ('B', 18)\n  ('D', 19)\n  ('A', 19)\n  ('C', 21)\n  ('E', 23)\n

    Adaptability: Adaptive sorting can utilize the existing order information in the input data to reduce the amount of computation, achieving better time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than the average time complexity.

    Comparison-based or non-comparison: Comparison-based sorting relies on comparison operators (\\(<\\), \\(=\\), \\(>\\)) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of \\(O(n \\log n)\\). Non-comparison sorting does not use comparison operators and can achieve a time complexity of \\(O(n)\\), but its versatility is relatively limited.

    ","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112-ideal-sorting-algorithm","level":2,"title":"11.1.2   Ideal Sorting Algorithm","text":"

    Fast, in-place, stable, adaptive, and broadly applicable. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem.

    Next, we will examine various sorting algorithms and analyze their advantages and disadvantages based on the evaluation dimensions above.

    ","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   Summary","text":"","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_sorting/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to \\(O(n)\\).
    • In each round, insertion sort inserts an element from the unsorted portion into its correct position in the sorted portion. Although insertion sort has a time complexity of \\(O(n^2)\\), it remains very popular for small sorting tasks because each operation is relatively lightweight.
    • Quick sort relies on sentinel partitioning. In sentinel partitioning, repeatedly choosing the worst possible pivot can degrade the time complexity to \\(O(n^2)\\). Choosing a median-based pivot or a random pivot can reduce the probability of this degradation. By recursing on the shorter subarray first, we can effectively reduce the recursion depth and optimize the space complexity to \\(O(\\log n)\\).
    • Merge sort includes two phases: divide and merge, which typically embody the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, with a space complexity of \\(O(n)\\); however, the space complexity of sorting a linked list can be optimized to \\(O(1)\\).
    • Bucket sort consists of three steps: distributing data into buckets, sorting within buckets, and merging results. It also embodies the divide-and-conquer strategy and is suitable for very large data volumes. The key to bucket sort is distributing data evenly.
    • Counting sort is a special case of bucket sort, which achieves sorting by counting the number of occurrences of data. Counting sort is suitable for situations where the data volume is large but the data range is limited, and requires that data can be converted to positive integers.
    • Radix sort achieves data sorting by sorting digit by digit, requiring that data can be represented as fixed-digit numbers.
    • Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive. However, as with other data structures and algorithms, no sorting algorithm can satisfy all of these criteria at the same time. In practice, we need to choose the appropriate sorting algorithm based on the characteristics of the data.
    • Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place property, and adaptability.

    Figure 11-19   Sorting algorithm comparison

    ","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: In what situations is the stability of sorting algorithms necessary?

    In reality, we may sort based on a certain attribute of objects. For example, students have two attributes: name and height. We want to implement multi-level sorting: first sort by name to get (A, 180) (B, 185) (C, 170) (D, 170); then sort by height. Because the sorting algorithm is unstable, we may get (D, 170) (C, 170) (A, 180) (B, 185).

    We can see that students D and C have swapped positions, destroying the ordering by name, which is not what we want.

    Q: Can the order of \"searching from right to left\" and \"searching from left to right\" in sentinel partitioning be swapped?

    No. When we use the leftmost element as the pivot, we must first \"search from right to left\" and then \"search from left to right\". This conclusion is somewhat counterintuitive; let's analyze the reason.

    The last step of sentinel partitioning partition() is to swap nums[left] and nums[i]. After the swap is complete, the elements to the left of the pivot are all <= the pivot, which requires that nums[left] >= nums[i] must hold before the last swap. Suppose we first \"search from left to right\", then if we cannot find an element larger than the pivot, we will exit the loop when i == j, at which point it may be that nums[j] == nums[i] > nums[left]. In other words, the last swap operation will swap an element larger than the pivot to the leftmost end of the array, causing sentinel partitioning to fail.

    For example, given the array [0, 0, 0, 0, 1], if we first \"search from left to right\", the array after sentinel partitioning is [1, 0, 0, 0, 0], which is incorrect.

    By the same reasoning, if we select nums[right] as the pivot, the order is reversed: we must first \"search from left to right\".

    Q: Regarding the optimization of recursion depth in quick sort, why can selecting the shorter array ensure that the recursion depth does not exceed \\(\\log n\\)?

    Recursion depth is the number of recursive calls that have not yet returned. Each round of sentinel partitioning divides the original array into two sub-arrays. After this optimization, the sub-array selected for further recursion is at most half the length of the original array. In the worst case, if it is always half as long, the final recursion depth is \\(\\log n\\).

    Reviewing the original quick sort, we may continuously recurse on the longer array. In the worst case, it would be \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(2\\), \\(1\\), with a recursion depth of \\(n\\). Recursion depth optimization can avoid this situation.

    Q: When all elements in the array are equal, is the time complexity of quick sort \\(O(n^2)\\)? How should this degenerate case be handled?

    Yes. In this case, the array can be partitioned into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. We then recurse only on the less-than and greater-than parts. With this approach, an array whose elements are all equal can be sorted in just one round of sentinel partitioning.

    Q: Why is the worst-case time complexity of bucket sort \\(O(n^2)\\)?

    In the worst case, all elements are distributed into the same bucket. If we use an \\(O(n^2)\\) algorithm to sort these elements, the time complexity will be \\(O(n^2)\\).

    ","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"Chapter 5.   Stacks and Queues","text":"

    Abstract

    A stack is like cats piled on top of one another, while a queue is like cats lining up.

    They represent the logical relationships of LIFO (Last In, First Out) and FIFO (First In, First Out), respectively.

    ","path":["Chapter 5. Stacks and Queues","Chapter 5.   Stacks and Queues"],"tags":[]},{"location":"chapter_stack_and_queue/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 5.1   Stack
    • 5.2   Queue
    • 5.3   Deque
    • 5.4   Summary
    ","path":["Chapter 5. Stacks and Queues","Chapter 5.   Stacks and Queues"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   Deque","text":"

    In a queue, we can only remove elements from the front or add elements at the rear. As shown in Figure 5-7, a double-ended queue (deque) provides greater flexibility, allowing elements to be added or removed at both the front and the rear.

    Figure 5-7   Operations of deque

    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531-common-deque-operations","level":2,"title":"5.3.1   Common Deque Operations","text":"

    The common operations on a deque are shown in Table 5-3. The specific method names depend on the programming language used.

    Table 5-3   Efficiency of Deque Operations

    Method Description Time Complexity push_first() Add element to front \\(O(1)\\) push_last() Add element to rear \\(O(1)\\) pop_first() Remove front element \\(O(1)\\) pop_last() Remove rear element \\(O(1)\\) peek_first() Access front element \\(O(1)\\) peek_last() Access rear element \\(O(1)\\)

    Similarly, we can directly use the deque classes provided by the programming language:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py
    from collections import deque\n\n# Initialize deque\ndeq: deque[int] = deque()\n\n# Enqueue elements\ndeq.append(2)      # Add to rear\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3)  # Add to front\ndeq.appendleft(1)\n\n# Access elements\nfront: int = deq[0]  # Front element\nrear: int = deq[-1]  # Rear element\n\n# Dequeue elements\npop_front: int = deq.popleft()  # Front element dequeue\npop_rear: int = deq.pop()       # Rear element dequeue\n\n# Get deque length\nsize: int = len(deq)\n\n# Check if deque is empty\nis_empty: bool = len(deq) == 0\n
    deque.cpp
    /* Initialize deque */\ndeque<int> deque;\n\n/* Enqueue elements */\ndeque.push_back(2);   // Add to rear\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3);  // Add to front\ndeque.push_front(1);\n\n/* Access elements */\nint front = deque.front(); // Front element\nint back = deque.back();   // Rear element\n\n/* Dequeue elements */\ndeque.pop_front();  // Front element dequeue\ndeque.pop_back();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.size();\n\n/* Check if deque is empty */\nbool empty = deque.empty();\n
    deque.java
    /* Initialize deque */\nDeque<Integer> deque = new LinkedList<>();\n\n/* Enqueue elements */\ndeque.offerLast(2);   // Add to rear\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3);  // Add to front\ndeque.offerFirst(1);\n\n/* Access elements */\nint peekFirst = deque.peekFirst();  // Front element\nint peekLast = deque.peekLast();    // Rear element\n\n/* Dequeue elements */\nint popFirst = deque.pollFirst();  // Front element dequeue\nint popLast = deque.pollLast();    // Rear element dequeue\n\n/* Get deque length */\nint size = deque.size();\n\n/* Check if deque is empty */\nboolean isEmpty = deque.isEmpty();\n
    deque.cs
    /* Initialize deque */\n// In C#, use LinkedList as a deque\nLinkedList<int> deque = new();\n\n/* Enqueue elements */\ndeque.AddLast(2);   // Add to rear\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3);  // Add to front\ndeque.AddFirst(1);\n\n/* Access elements */\nint peekFirst = deque.First.Value;  // Front element\nint peekLast = deque.Last.Value;    // Rear element\n\n/* Dequeue elements */\ndeque.RemoveFirst();  // Front element dequeue\ndeque.RemoveLast();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.Count;\n\n/* Check if deque is empty */\nbool isEmpty = deque.Count == 0;\n
    deque_test.go
    /* Initialize deque */\n// In Go, use list as a deque\ndeque := list.New()\n\n/* Enqueue elements */\ndeque.PushBack(2)      // Add to rear\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3)     // Add to front\ndeque.PushFront(1)\n\n/* Access elements */\nfront := deque.Front() // Front element\nrear := deque.Back()   // Rear element\n\n/* Dequeue elements */\ndeque.Remove(front)    // Front element dequeue\ndeque.Remove(rear)     // Rear element dequeue\n\n/* Get deque length */\nsize := deque.Len()\n\n/* Check if deque is empty */\nisEmpty := deque.Len() == 0\n
    deque.swift
    /* Initialize deque */\n// Swift does not have a built-in deque class, can use Array as a deque\nvar deque: [Int] = []\n\n/* Enqueue elements */\ndeque.append(2) // Add to rear\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // Add to front\ndeque.insert(1, at: 0)\n\n/* Access elements */\nlet peekFirst = deque.first! // Front element\nlet peekLast = deque.last! // Rear element\n\n/* Dequeue elements */\n// When using Array simulation, popFirst has O(n) complexity\nlet popFirst = deque.removeFirst() // Front element dequeue\nlet popLast = deque.removeLast() // Rear element dequeue\n\n/* Get deque length */\nlet size = deque.count\n\n/* Check if deque is empty */\nlet isEmpty = deque.isEmpty\n
    deque.js
    /* Initialize deque */\n// JavaScript does not have a built-in deque, can only use Array as a deque\nconst deque = [];\n\n/* Enqueue elements */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Please note that since it's an array, unshift() has O(n) time complexity\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Access elements */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* Dequeue elements */\n// Please note that since it's an array, shift() has O(n) time complexity\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* Get deque length */\nconst size = deque.length;\n\n/* Check if deque is empty */\nconst isEmpty = size === 0;\n
    deque.ts
    /* Initialize deque */\n// TypeScript does not have a built-in deque, can only use Array as a deque\nconst deque: number[] = [];\n\n/* Enqueue elements */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Please note that since it's an array, unshift() has O(n) time complexity\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Access elements */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* Dequeue elements */\n// Please note that since it's an array, shift() has O(n) time complexity\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* Get deque length */\nconst size: number = deque.length;\n\n/* Check if deque is empty */\nconst isEmpty: boolean = size === 0;\n
    deque.dart
    /* Initialize deque */\n// In Dart, Queue is defined as a deque\nQueue<int> deque = Queue<int>();\n\n/* Enqueue elements */\ndeque.addLast(2);  // Add to rear\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // Add to front\ndeque.addFirst(1);\n\n/* Access elements */\nint peekFirst = deque.first; // Front element\nint peekLast = deque.last;   // Rear element\n\n/* Dequeue elements */\nint popFirst = deque.removeFirst(); // Front element dequeue\nint popLast = deque.removeLast();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.length;\n\n/* Check if deque is empty */\nbool isEmpty = deque.isEmpty;\n
    deque.rs
    /* Initialize deque */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Enqueue elements */\ndeque.push_back(2);  // Add to rear\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // Add to front\ndeque.push_front(1);\n\n/* Access elements */\nif let Some(front) = deque.front() { // Front element\n}\nif let Some(rear) = deque.back() {   // Rear element\n}\n\n/* Dequeue elements */\nif let Some(pop_front) = deque.pop_front() { // Front element dequeue\n}\nif let Some(pop_rear) = deque.pop_back() {   // Rear element dequeue\n}\n\n/* Get deque length */\nlet size = deque.len();\n\n/* Check if deque is empty */\nlet is_empty = deque.is_empty();\n
    deque.c
    // C does not provide a built-in deque\n
    deque.kt
    /* Initialize deque */\nval deque = LinkedList<Int>()\n\n/* Enqueue elements */\ndeque.offerLast(2)  // Add to rear\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // Add to front\ndeque.offerFirst(1)\n\n/* Access elements */\nval peekFirst = deque.peekFirst() // Front element\nval peekLast = deque.peekLast()   // Rear element\n\n/* Dequeue elements */\nval popFirst = deque.pollFirst() // Front element dequeue\nval popLast = deque.pollLast()   // Rear element dequeue\n\n/* Get deque length */\nval size = deque.size\n\n/* Check if deque is empty */\nval isEmpty = deque.isEmpty()\n
    deque.rb
    # Initialize deque\n# Ruby does not have a built-in deque, can only use Array as a deque\ndeque = []\n\n# Enqueue elements\ndeque << 2\ndeque << 5\ndeque << 4\n# Please note that since it's an array, Array#unshift has O(n) time complexity\ndeque.unshift(3)\ndeque.unshift(1)\n\n# Access elements\npeek_first = deque.first\npeek_last = deque.last\n\n# Dequeue elements\n# Please note that since it's an array, Array#shift has O(n) time complexity\npop_front = deque.shift\npop_back = deque.pop\n\n# Get deque length\nsize = deque.length\n\n# Check if deque is empty\nis_empty = size.zero?\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532-deque-implementation","level":2,"title":"5.3.2   Deque Implementation *","text":"

    The implementation of a deque is similar to that of a queue. You can choose either a linked list or an array as the underlying data structure.

    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1-doubly-linked-list-implementation","level":3,"title":"1.   Doubly Linked List Implementation","text":"

    Reviewing the previous section, we used a regular singly linked list to implement a queue because it conveniently allows deleting the head node (corresponding to dequeue) and adding new nodes after the tail node (corresponding to enqueue).

    For a deque, both the front and rear can perform enqueue and dequeue operations. In other words, a deque needs to implement operations in the opposite direction as well. For this reason, we use a \"doubly linked list\" as the underlying data structure for the deque.

    As shown in Figure 5-8, we treat the head and tail nodes of the doubly linked list as the front and rear of the deque, implementing functionality to add and remove nodes at both ends.

    <1><2><3><4><5>

    Figure 5-8   Enqueue and dequeue operations in linked list implementation of deque

    The implementation code is shown below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py
    class ListNode:\n    \"\"\"Doubly linked list node\"\"\"\n\n    def __init__(self, val: int):\n        \"\"\"Constructor\"\"\"\n        self.val: int = val\n        self.next: ListNode | None = None  # Successor node reference\n        self.prev: ListNode | None = None  # Predecessor node reference\n\nclass LinkedListDeque:\n    \"\"\"Double-ended queue based on doubly linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._front: ListNode | None = None  # Head node front\n        self._rear: ListNode | None = None  # Tail node rear\n        self._size: int = 0  # Length of the double-ended queue\n\n    def size(self) -> int:\n        \"\"\"Get the length of the double-ended queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the double-ended queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int, is_front: bool):\n        \"\"\"Enqueue operation\"\"\"\n        node = ListNode(num)\n        # If the linked list is empty, make both front and rear point to node\n        if self.is_empty():\n            self._front = self._rear = node\n        # Front of the queue enqueue operation\n        elif is_front:\n            # Add node to the head of the linked list\n            self._front.prev = node\n            node.next = self._front\n            self._front = node  # Update head node\n        # Rear of the queue enqueue operation\n        else:\n            # Add node to the tail of the linked list\n            self._rear.next = node\n            node.prev = self._rear\n            self._rear = node  # Update tail node\n        self._size += 1  # Update queue length\n\n    def push_first(self, num: int):\n        \"\"\"Front of the queue enqueue\"\"\"\n        self.push(num, True)\n\n    def push_last(self, num: int):\n        \"\"\"Rear of the queue enqueue\"\"\"\n        self.push(num, False)\n\n    def pop(self, is_front: bool) -> int:\n        \"\"\"Dequeue operation\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        # Front of the queue dequeue operation\n        if is_front:\n            val: int = self._front.val  # Temporarily store head node value\n            # Delete head node\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  # Update head node\n        # Rear of the queue dequeue operation\n        else:\n            val: int = self._rear.val  # Temporarily store tail node value\n            # Delete tail node\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  # Update tail node\n        self._size -= 1  # Update queue length\n        return val\n\n    def pop_first(self) -> int:\n        \"\"\"Front of the queue dequeue\"\"\"\n        return self.pop(True)\n\n    def pop_last(self) -> int:\n        \"\"\"Rear of the queue dequeue\"\"\"\n        return self.pop(False)\n\n    def peek_first(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._front.val\n\n    def peek_last(self) -> int:\n        \"\"\"Access rear of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._rear.val\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return array for printing\"\"\"\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
    /* Doubly linked list node */\nstruct DoublyListNode {\n    int val;              // Node value\n    DoublyListNode *next; // Successor node pointer\n    DoublyListNode *prev; // Predecessor node pointer\n    DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n    }\n};\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n  private:\n    DoublyListNode *front, *rear; // Head node front, tail node rear\n    int queSize = 0;              // Length of the double-ended queue\n\n  public:\n    /* Constructor */\n    LinkedListDeque() : front(nullptr), rear(nullptr) {\n    }\n\n    /* Destructor */\n    ~LinkedListDeque() {\n        // Traverse linked list to delete nodes and free memory\n        DoublyListNode *pre, *cur = front;\n        while (cur != nullptr) {\n            pre = cur;\n            cur = cur->next;\n            delete pre;\n        }\n    }\n\n    /* Get the length of the double-ended queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue operation */\n    void push(int num, bool isFront) {\n        DoublyListNode *node = new DoublyListNode(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty())\n            front = rear = node;\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front->prev = node;\n            node->next = front;\n            front = node; // Update head node\n        // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear->next = node;\n            node->prev = rear;\n            rear = node; // Update tail node\n        }\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Dequeue operation */\n    int pop(bool isFront) {\n        if (isEmpty())\n            throw out_of_range(\"Queue is empty\");\n        int val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front->val; // Delete head node\n            // Delete head node\n            DoublyListNode *fNext = front->next;\n            if (fNext != nullptr) {\n                fNext->prev = nullptr;\n                front->next = nullptr;\n            }\n            delete front;\n            front = fNext; // Update head node\n        // Temporarily store tail node value\n        } else {\n            val = rear->val; // Delete tail node\n            // Update tail node\n            DoublyListNode *rPrev = rear->prev;\n            if (rPrev != nullptr) {\n                rPrev->next = nullptr;\n                rear->prev = nullptr;\n            }\n            delete rear;\n            rear = rPrev; // Update tail node\n        }\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    int popFirst() {\n        return pop(true);\n    }\n\n    /* Access rear of the queue element */\n    int popLast() {\n        return pop(false);\n    }\n\n    /* Return list for printing */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return front->val;\n    }\n\n    /* Driver Code */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return rear->val;\n    }\n\n    /* Return array for printing */\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
    /* Doubly linked list node */\nclass ListNode {\n    int val; // Node value\n    ListNode next; // Successor node reference\n    ListNode prev; // Predecessor node reference\n\n    ListNode(int val) {\n        this.val = val;\n        prev = next = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private ListNode front, rear; // Head node front, tail node rear\n    private int queSize = 0; // Length of the double-ended queue\n\n    public LinkedListDeque() {\n        front = rear = null;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue operation */\n    private void push(int num, boolean isFront) {\n        ListNode node = new ListNode(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty())\n            front = rear = node;\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front.prev = node;\n            node.next = front;\n            front = node; // Update head node\n        // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear.next = node;\n            node.prev = rear;\n            rear = node; // Update tail node\n        }\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    public void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    public void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Dequeue operation */\n    private int pop(boolean isFront) {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        int val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front.val; // Delete head node\n            // Delete head node\n            ListNode fNext = front.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front.next = null;\n            }\n            front = fNext; // Update head node\n        // Temporarily store tail node value\n        } else {\n            val = rear.val; // Delete tail node\n            // Update tail node\n            ListNode rPrev = rear.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear.prev = null;\n            }\n            rear = rPrev; // Update tail node\n        }\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    public int popFirst() {\n        return pop(true);\n    }\n\n    /* Access rear of the queue element */\n    public int popLast() {\n        return pop(false);\n    }\n\n    /* Return list for printing */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Driver Code */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return rear.val;\n    }\n\n    /* Return array for printing */\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
    /* Doubly linked list node */\nclass ListNode(int val) {\n    public int val = val;       // Node value\n    public ListNode? next = null; // Successor node reference\n    public ListNode? prev = null; // Predecessor node reference\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    ListNode? front, rear; // Head node front, tail node rear\n    int queSize = 0;      // Length of the double-ended queue\n\n    public LinkedListDeque() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Enqueue operation */\n    void Push(int num, bool isFront) {\n        ListNode node = new(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (IsEmpty()) {\n            front = node;\n            rear = node;\n        }\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front!.prev = node;\n            node.next = front;\n            front = node; // Update head node\n        }\n        // Rear of the queue enqueue operation\n        else {\n            // Add node to the tail of the linked list\n            rear!.next = node;\n            node.prev = rear;\n            rear = node;  // Update tail node\n        }\n\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    public void PushFirst(int num) {\n        Push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    public void PushLast(int num) {\n        Push(num, false);\n    }\n\n    /* Dequeue operation */\n    int? Pop(bool isFront) {\n        if (IsEmpty())\n            throw new Exception();\n        int? val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front?.val; // Delete head node\n            // Delete head node\n            ListNode? fNext = front?.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front!.next = null;\n            }\n            front = fNext;   // Update head node\n        }\n        // Temporarily store tail node value\n        else {\n            val = rear?.val;  // Delete tail node\n            // Update tail node\n            ListNode? rPrev = rear?.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear!.prev = null;\n            }\n            rear = rPrev;    // Update tail node\n        }\n\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    public int? PopFirst() {\n        return Pop(true);\n    }\n\n    /* Access rear of the queue element */\n    public int? PopLast() {\n        return Pop(false);\n    }\n\n    /* Return list for printing */\n    public int? PeekFirst() {\n        if (IsEmpty())\n            throw new Exception();\n        return front?.val;\n    }\n\n    /* Driver Code */\n    public int? PeekLast() {\n        if (IsEmpty())\n            throw new Exception();\n        return rear?.val;\n    }\n\n    /* Return array for printing */\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
    /* Double-ended queue based on doubly linked list implementation */\ntype linkedListDeque struct {\n    // Use built-in package list\n    data *list.List\n}\n\n/* Initialize deque */\nfunc newLinkedListDeque() *linkedListDeque {\n    return &linkedListDeque{\n        data: list.New(),\n    }\n}\n\n/* Front element enqueue */\nfunc (s *linkedListDeque) pushFirst(value any) {\n    s.data.PushFront(value)\n}\n\n/* Rear element enqueue */\nfunc (s *linkedListDeque) pushLast(value any) {\n    s.data.PushBack(value)\n}\n\n/* Check if the double-ended queue is empty */\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/* Rear element dequeue */\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/* Return list for printing */\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/* Driver Code */\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/* Get the length of the queue */\nfunc (s *linkedListDeque) size() int {\n    return s.data.Len()\n}\n\n/* Check if the queue is empty */\nfunc (s *linkedListDeque) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListDeque) toList() *list.List {\n    return s.data\n}\n
    linkedlist_deque.swift
    /* Doubly linked list node */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Successor node reference\n    weak var prev: ListNode? // Predecessor node reference\n\n    init(val: Int) {\n        self.val = val\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private var front: ListNode? // Head node front\n    private var rear: ListNode? // Tail node rear\n    private var _size: Int // Length of the double-ended queue\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the double-ended queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the double-ended queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue operation */\n    private func push(num: Int, isFront: Bool) {\n        let node = ListNode(val: num)\n        // If the linked list is empty, make both front and rear point to node\n        if isEmpty() {\n            front = node\n            rear = node\n        }\n        // Front of the queue enqueue operation\n        else if isFront {\n            // Add node to the head of the linked list\n            front?.prev = node\n            node.next = front\n            front = node // Update head node\n        }\n        // Rear of the queue enqueue operation\n        else {\n            // Add node to the tail of the linked list\n            rear?.next = node\n            node.prev = rear\n            rear = node // Update tail node\n        }\n        _size += 1 // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    func pushFirst(num: Int) {\n        push(num: num, isFront: true)\n    }\n\n    /* Rear of the queue enqueue */\n    func pushLast(num: Int) {\n        push(num: num, isFront: false)\n    }\n\n    /* Dequeue operation */\n    private func pop(isFront: Bool) -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        let val: Int\n        // Temporarily store head node value\n        if isFront {\n            val = front!.val // Delete head node\n            // Delete head node\n            let fNext = front?.next\n            if fNext != nil {\n                fNext?.prev = nil\n                front?.next = nil\n            }\n            front = fNext // Update head node\n        }\n        // Temporarily store tail node value\n        else {\n            val = rear!.val // Delete tail node\n            // Update tail node\n            let rPrev = rear?.prev\n            if rPrev != nil {\n                rPrev?.next = nil\n                rear?.prev = nil\n            }\n            rear = rPrev // Update tail node\n        }\n        _size -= 1 // Update queue length\n        return val\n    }\n\n    /* Rear of the queue dequeue */\n    func popFirst() -> Int {\n        pop(isFront: true)\n    }\n\n    /* Access rear of the queue element */\n    func popLast() -> Int {\n        pop(isFront: false)\n    }\n\n    /* Return list for printing */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return front!.val\n    }\n\n    /* Driver Code */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return rear!.val\n    }\n\n    /* Return array for printing */\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
    /* Doubly linked list node */\nclass ListNode {\n    prev; // Predecessor node reference (pointer)\n    next; // Successor node reference (pointer)\n    val; // Node value\n\n    constructor(val) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    #front; // Head node front\n    #rear; // Tail node rear\n    #queSize; // Length of the double-ended queue\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n        this.#queSize = 0;\n    }\n\n    /* Rear of the queue enqueue operation */\n    pushLast(val) {\n        const node = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Add node to the tail of the linked list\n            this.#rear.next = node;\n            node.prev = this.#rear;\n            this.#rear = node; // Update tail node\n        }\n        this.#queSize++;\n    }\n\n    /* Front of the queue enqueue operation */\n    pushFirst(val) {\n        const node = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Add node to the head of the linked list\n            this.#front.prev = node;\n            node.next = this.#front;\n            this.#front = node; // Update head node\n        }\n        this.#queSize++;\n    }\n\n    /* Temporarily store tail node value */\n    popLast() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#rear.val; // Store tail node value\n        // Update tail node\n        let temp = this.#rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.#rear.prev = null;\n        }\n        this.#rear = temp; // Update tail node\n        this.#queSize--;\n        return value;\n    }\n\n    /* Temporarily store head node value */\n    popFirst() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#front.val; // Store tail node value\n        // Delete head node\n        let temp = this.#front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.#front.next = null;\n        }\n        this.#front = temp; // Update head node\n        this.#queSize--;\n        return value;\n    }\n\n    /* Driver Code */\n    peekLast() {\n        return this.#queSize === 0 ? null : this.#rear.val;\n    }\n\n    /* Return list for printing */\n    peekFirst() {\n        return this.#queSize === 0 ? null : this.#front.val;\n    }\n\n    /* Get the length of the double-ended queue */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Print deque */\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
    /* Doubly linked list node */\nclass ListNode {\n    prev: ListNode; // Predecessor node reference (pointer)\n    next: ListNode; // Successor node reference (pointer)\n    val: number; // Node value\n\n    constructor(val: number) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private front: ListNode; // Head node front\n    private rear: ListNode; // Tail node rear\n    private queSize: number; // Length of the double-ended queue\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n        this.queSize = 0;\n    }\n\n    /* Rear of the queue enqueue operation */\n    pushLast(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Add node to the tail of the linked list\n            this.rear.next = node;\n            node.prev = this.rear;\n            this.rear = node; // Update tail node\n        }\n        this.queSize++;\n    }\n\n    /* Front of the queue enqueue operation */\n    pushFirst(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Add node to the head of the linked list\n            this.front.prev = node;\n            node.next = this.front;\n            this.front = node; // Update head node\n        }\n        this.queSize++;\n    }\n\n    /* Temporarily store tail node value */\n    popLast(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.rear.val; // Store tail node value\n        // Update tail node\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; // Update tail node\n        this.queSize--;\n        return value;\n    }\n\n    /* Temporarily store head node value */\n    popFirst(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.front.val; // Store tail node value\n        // Delete head node\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; // Update head node\n        this.queSize--;\n        return value;\n    }\n\n    /* Driver Code */\n    peekLast(): number {\n        return this.queSize === 0 ? null : this.rear.val;\n    }\n\n    /* Return list for printing */\n    peekFirst(): number {\n        return this.queSize === 0 ? null : this.front.val;\n    }\n\n    /* Get the length of the double-ended queue */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Print deque */\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
    /* Doubly linked list node */\nclass ListNode {\n  int val; // Node value\n  ListNode? next; // Successor node reference\n  ListNode? prev; // Predecessor node reference\n\n  ListNode(this.val, {this.next, this.prev});\n}\n\n/* Deque implemented based on doubly linked list */\nclass LinkedListDeque {\n  late ListNode? _front; // Head node _front\n  late ListNode? _rear; // Tail node _rear\n  int _queSize = 0; // Length of the double-ended queue\n\n  LinkedListDeque() {\n    this._front = null;\n    this._rear = null;\n  }\n\n  /* Get deque length */\n  int size() {\n    return this._queSize;\n  }\n\n  /* Check if the double-ended queue is empty */\n  bool isEmpty() {\n    return size() == 0;\n  }\n\n  /* Enqueue operation */\n  void push(int _num, bool isFront) {\n    final ListNode node = ListNode(_num);\n    if (isEmpty()) {\n      // If list is empty, let both _front and _rear point to node\n      _front = _rear = node;\n    } else if (isFront) {\n      // Front of the queue enqueue operation\n      // Add node to the head of the linked list\n      _front!.prev = node;\n      node.next = _front;\n      _front = node; // Update head node\n    } else {\n      // Rear of the queue enqueue operation\n      // Add node to the tail of the linked list\n      _rear!.next = node;\n      node.prev = _rear;\n      _rear = node; // Update tail node\n    }\n    _queSize++; // Update queue length\n  }\n\n  /* Front of the queue enqueue */\n  void pushFirst(int _num) {\n    push(_num, true);\n  }\n\n  /* Rear of the queue enqueue */\n  void pushLast(int _num) {\n    push(_num, false);\n  }\n\n  /* Dequeue operation */\n  int? pop(bool isFront) {\n    // If queue is empty, return null directly\n    if (isEmpty()) {\n      return null;\n    }\n    final int val;\n    if (isFront) {\n      // Temporarily store head node value\n      val = _front!.val; // Delete head node\n      // Delete head node\n      ListNode? fNext = _front!.next;\n      if (fNext != null) {\n        fNext.prev = null;\n        _front!.next = null;\n      }\n      _front = fNext; // Update head node\n    } else {\n      // Temporarily store tail node value\n      val = _rear!.val; // Delete tail node\n      // Update tail node\n      ListNode? rPrev = _rear!.prev;\n      if (rPrev != null) {\n        rPrev.next = null;\n        _rear!.prev = null;\n      }\n      _rear = rPrev; // Update tail node\n    }\n    _queSize--; // Update queue length\n    return val;\n  }\n\n  /* Rear of the queue dequeue */\n  int? popFirst() {\n    return pop(true);\n  }\n\n  /* Access rear of the queue element */\n  int? popLast() {\n    return pop(false);\n  }\n\n  /* Return list for printing */\n  int? peekFirst() {\n    return _front?.val;\n  }\n\n  /* Driver Code */\n  int? peekLast() {\n    return _rear?.val;\n  }\n\n  /* Return array for printing */\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
    /* Doubly linked list node */\npub struct ListNode<T> {\n    pub val: T,                                 // Node value\n    pub next: Option<Rc<RefCell<ListNode<T>>>>, // Successor node pointer\n    pub prev: Option<Rc<RefCell<ListNode<T>>>>, // Predecessor node pointer\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/* Double-ended queue based on doubly linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListDeque<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Head node front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Tail node rear\n    que_size: usize,                         // Length of the double-ended queue\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    /* Get the length of the double-ended queue */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Check if the double-ended queue is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Enqueue operation */\n    fn push(&mut self, num: T, is_front: bool) {\n        let node = ListNode::new(num);\n        // Front of the queue enqueue operation\n        if is_front {\n            match self.front.take() {\n                // If the linked list is empty, make both front and rear point to node\n                None => {\n                    self.rear = Some(node.clone());\n                    self.front = Some(node);\n                }\n                // Add node to the head of the linked list\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); // Update head node\n                }\n            }\n        }\n        // Rear of the queue enqueue operation\n        else {\n            match self.rear.take() {\n                // If the linked list is empty, make both front and rear point to node\n                None => {\n                    self.front = Some(node.clone());\n                    self.rear = Some(node);\n                }\n                // Add node to the tail of the linked list\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); // Update tail node\n                }\n            }\n        }\n        self.que_size += 1; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    pub fn push_first(&mut self, num: T) {\n        self.push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    pub fn push_last(&mut self, num: T) {\n        self.push(num, false);\n    }\n\n    /* Dequeue operation */\n    fn pop(&mut self, is_front: bool) -> Option<T> {\n        // If queue is empty, return None directly\n        if self.is_empty() {\n            return None;\n        };\n        // Temporarily store head node value\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); // Update head node\n                    }\n                    None => {\n                        self.rear.take();\n                    }\n                }\n                self.que_size -= 1; // Update queue length\n                old_front.borrow().val\n            })\n        }\n        // Temporarily store tail node value\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); // Update tail node\n                    }\n                    None => {\n                        self.front.take();\n                    }\n                }\n                self.que_size -= 1; // Update queue length\n                old_rear.borrow().val\n            })\n        }\n    }\n\n    /* Rear of the queue dequeue */\n    pub fn pop_first(&mut self) -> Option<T> {\n        return self.pop(true);\n    }\n\n    /* Access rear of the queue element */\n    pub fn pop_last(&mut self) -> Option<T> {\n        return self.pop(false);\n    }\n\n    /* Return list for printing */\n    pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Driver Code */\n    pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.rear.as_ref()\n    }\n\n    /* Return array for printing */\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
    /* Doubly linked list node */\ntypedef struct DoublyListNode {\n    int val;                     // Node value\n    struct DoublyListNode *next; // Successor node\n    struct DoublyListNode *prev; // Predecessor node\n} DoublyListNode;\n\n/* Constructor */\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/* Destructor */\nvoid delDoublyListNode(DoublyListNode *node) {\n    free(node);\n}\n\n/* Double-ended queue based on doubly linked list implementation */\ntypedef struct {\n    DoublyListNode *front, *rear; // Head node front, tail node rear\n    int queSize;                  // Length of the double-ended queue\n} LinkedListDeque;\n\n/* Constructor */\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/* Destructor */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n    // Free all nodes\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    // Free deque structure\n    free(deque);\n}\n\n/* Get the length of the queue */\nint size(LinkedListDeque *deque) {\n    return deque->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(LinkedListDeque *deque) {\n    return (size(deque) == 0);\n}\n\n/* Enqueue */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n    DoublyListNode *node = newDoublyListNode(num);\n    // If list is empty, set both front and rear to node\n    if (empty(deque)) {\n        deque->front = deque->rear = node;\n    }\n    // Front of the queue enqueue operation\n    else if (isFront) {\n        // Add node to the head of the linked list\n        deque->front->prev = node;\n        node->next = deque->front;\n        deque->front = node; // Update head node\n    }\n    // Rear of the queue enqueue operation\n    else {\n        // Add node to the tail of the linked list\n        deque->rear->next = node;\n        node->prev = deque->rear;\n        deque->rear = node;\n    }\n    deque->queSize++; // Update queue length\n}\n\n/* Front of the queue enqueue */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n    push(deque, num, true);\n}\n\n/* Rear of the queue enqueue */\nvoid pushLast(LinkedListDeque *deque, int num) {\n    push(deque, num, false);\n}\n\n/* Return list for printing */\nint peekFirst(LinkedListDeque *deque) {\n    assert(size(deque) && deque->front);\n    return deque->front->val;\n}\n\n/* Driver Code */\nint peekLast(LinkedListDeque *deque) {\n    assert(size(deque) && deque->rear);\n    return deque->rear->val;\n}\n\n/* Dequeue */\nint pop(LinkedListDeque *deque, bool isFront) {\n    if (empty(deque))\n        return -1;\n    int val;\n    // Temporarily store head node value\n    if (isFront) {\n        val = peekFirst(deque); // Delete head node\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; // Update head node\n    }\n    // Temporarily store tail node value\n    else {\n        val = peekLast(deque); // Delete tail node\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; // Update tail node\n    }\n    deque->queSize--; // Update queue length\n    return val;\n}\n\n/* Rear of the queue dequeue */\nint popFirst(LinkedListDeque *deque) {\n    return pop(deque, true);\n}\n\n/* Access rear of the queue element */\nint popLast(LinkedListDeque *deque) {\n    return pop(deque, false);\n}\n\n/* Print queue */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n    int *arr = malloc(sizeof(int) * deque->queSize);\n    // Copy data from list to array\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
    /* Doubly linked list node */\nclass ListNode(var _val: Int) {\n    // Node value\n    var next: ListNode? = null // Successor node reference\n    var prev: ListNode? = null // Predecessor node reference\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private var front: ListNode? = null // Head node front\n    private var rear: ListNode? = null // Tail node rear\n    private var queSize: Int = 0 // Length of the double-ended queue\n\n    /* Get the length of the double-ended queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the double-ended queue is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Enqueue operation */\n    fun push(num: Int, isFront: Boolean) {\n        val node = ListNode(num)\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty()) {\n            rear = node\n            front = rear\n            // Front of the queue enqueue operation\n        } else if (isFront) {\n            // Add node to the head of the linked list\n            front?.prev = node\n            node.next = front\n            front = node // Update head node\n            // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear?.next = node\n            node.prev = rear\n            rear = node // Update tail node\n        }\n        queSize++ // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    fun pushFirst(num: Int) {\n        push(num, true)\n    }\n\n    /* Rear of the queue enqueue */\n    fun pushLast(num: Int) {\n        push(num, false)\n    }\n\n    /* Dequeue operation */\n    fun pop(isFront: Boolean): Int {\n        if (isEmpty()) \n            throw IndexOutOfBoundsException()\n        val _val: Int\n        // Temporarily store head node value\n        if (isFront) {\n            _val = front!!._val // Delete head node\n            // Delete head node\n            val fNext = front!!.next\n            if (fNext != null) {\n                fNext.prev = null\n                front!!.next = null\n            }\n            front = fNext // Update head node\n            // Temporarily store tail node value\n        } else {\n            _val = rear!!._val // Delete tail node\n            // Update tail node\n            val rPrev = rear!!.prev\n            if (rPrev != null) {\n                rPrev.next = null\n                rear!!.prev = null\n            }\n            rear = rPrev // Update tail node\n        }\n        queSize-- // Update queue length\n        return _val\n    }\n\n    /* Rear of the queue dequeue */\n    fun popFirst(): Int {\n        return pop(true)\n    }\n\n    /* Access rear of the queue element */\n    fun popLast(): Int {\n        return pop(false)\n    }\n\n    /* Return list for printing */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Driver Code */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return rear!!._val\n    }\n\n    /* Return array for printing */\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### Doubly linked list node\nclass ListNode\n  attr_accessor :val\n  attr_accessor :next # Successor node reference\n  attr_accessor :prev # Predecessor node reference\n\n  ### Constructor ###\n  def initialize(val)\n    @val = val\n  end\nend\n\n### Deque based on doubly linked list ###\nclass LinkedListDeque\n  ### Get deque length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @front = nil  # Head node front\n    @rear = nil   # Tail node rear\n    @size = 0     # Length of the double-ended queue\n  end\n\n  ### Check if deque is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue operation ###\n  def push(num, is_front)\n    node = ListNode.new(num)\n    # If list is empty, set both front and rear to node\n    if is_empty?\n      @front = @rear = node\n    # Front of the queue enqueue operation\n    elsif is_front\n      # Add node to the head of the linked list\n      @front.prev = node\n      node.next = @front\n      @front = node # Update head node\n    # Rear of the queue enqueue operation\n    else\n      # Add node to the tail of the linked list\n      @rear.next = node\n      node.prev = @rear\n      @rear = node # Update tail node\n    end\n    @size += 1 # Update queue length\n  end\n\n  ### Enqueue at front ###\n  def push_first(num)\n    push(num, true)\n  end\n\n  ### Enqueue at rear ###\n  def push_last(num)\n    push(num, false)\n  end\n\n  ### Dequeue operation ###\n  def pop(is_front)\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    # Temporarily store head node value\n    if is_front\n      val = @front.val # Delete head node\n      # Delete head node\n      fnext = @front.next\n      unless fnext.nil?\n        fnext.prev = nil\n        @front.next = nil\n      end\n      @front = fnext # Update head node\n    # Temporarily store tail node value\n    else\n      val = @rear.val # Delete tail node\n      # Update tail node\n      rprev = @rear.prev\n      unless rprev.nil?\n        rprev.next = nil\n        @rear.prev = nil\n      end\n      @rear = rprev # Update tail node\n    end\n    @size -= 1 # Update queue length\n\n    val\n  end\n\n  ### Dequeue from front ###\n  def pop_first\n    pop(true)\n  end\n\n  ### Dequeue from front ###\n  def pop_last\n    pop(false)\n  end\n\n  ### Access front element ###\n  def peek_first\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @front.val\n  end\n\n  ### Access rear element ###\n  def peek_last\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @rear.val\n  end\n\n  ### Return array for printing ###\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":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

    As shown in Figure 5-9, similar to implementing a queue based on an array, we can also use a circular array to implement a deque.

    <1><2><3><4><5>

    Figure 5-9   Enqueue and dequeue operations in array implementation of deque

    Based on the queue implementation, we only need to add methods for \"enqueue at front\" and \"dequeue from rear\":

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py
    class ArrayDeque:\n    \"\"\"Double-ended queue based on circular array implementation\"\"\"\n\n    def __init__(self, capacity: int):\n        \"\"\"Constructor\"\"\"\n        self._nums: list[int] = [0] * capacity\n        self._front: int = 0\n        self._size: int = 0\n\n    def capacity(self) -> int:\n        \"\"\"Get the capacity of the double-ended queue\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Get the length of the double-ended queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the double-ended queue is empty\"\"\"\n        return self._size == 0\n\n    def index(self, i: int) -> int:\n        \"\"\"Calculate circular array index\"\"\"\n        # Use modulo operation to wrap the array head and tail together\n        # When i passes the tail of the array, return to the head\n        # When i passes the head of the array, return to the tail\n        return (i + self.capacity()) % self.capacity()\n\n    def push_first(self, num: int):\n        \"\"\"Front of the queue enqueue\"\"\"\n        if self._size == self.capacity():\n            print(\"Double-ended queue is full\")\n            return\n        # Front pointer moves one position to the left\n        # Use modulo operation to wrap front around to the tail after passing the head of the array\n        self._front = self.index(self._front - 1)\n        # Add num to the front of the queue\n        self._nums[self._front] = num\n        self._size += 1\n\n    def push_last(self, num: int):\n        \"\"\"Rear of the queue enqueue\"\"\"\n        if self._size == self.capacity():\n            print(\"Double-ended queue is full\")\n            return\n        # Calculate rear pointer, points to rear index + 1\n        rear = self.index(self._front + self._size)\n        # Add num to the rear of the queue\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop_first(self) -> int:\n        \"\"\"Front of the queue dequeue\"\"\"\n        num = self.peek_first()\n        # Front pointer moves one position backward\n        self._front = self.index(self._front + 1)\n        self._size -= 1\n        return num\n\n    def pop_last(self) -> int:\n        \"\"\"Rear of the queue dequeue\"\"\"\n        num = self.peek_last()\n        self._size -= 1\n        return num\n\n    def peek_first(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._nums[self._front]\n\n    def peek_last(self) -> int:\n        \"\"\"Access rear of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        # Calculate tail element index\n        last = self.index(self._front + self._size - 1)\n        return self._nums[last]\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return array for printing\"\"\"\n        # Only convert list elements within the valid length range\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n  private:\n    vector<int> nums; // Array for storing double-ended queue elements\n    int front;        // Front pointer, points to the front of the queue element\n    int queSize;      // Double-ended queue length\n\n  public:\n    /* Constructor */\n    ArrayDeque(int capacity) {\n        nums.resize(capacity);\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    int capacity() {\n        return nums.size();\n    }\n\n    /* Get the length of the double-ended queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    int index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity();\n    }\n\n    /* Front of the queue enqueue */\n    void pushFirst(int num) {\n        if (queSize == capacity()) {\n            cout << \"Double-ended queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    void pushLast(int num) {\n        if (queSize == capacity()) {\n            cout << \"Double-ended queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    int popFirst() {\n        int num = peekFirst();\n        // Move front pointer backward by one position\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return nums[front];\n    }\n\n    /* Driver Code */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        // Initialize double-ended queue\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    vector<int> toVector() {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private int[] nums; // Array for storing double-ended queue elements\n    private int front; // Front pointer, points to the front of the queue element\n    private int queSize; // Double-ended queue length\n\n    /* Constructor */\n    public ArrayDeque(int capacity) {\n        this.nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    private int index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity();\n    }\n\n    /* Front of the queue enqueue */\n    public void pushFirst(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    public void pushLast(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    public int popFirst() {\n        int num = peekFirst();\n        // Move front pointer backward by one position\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    public int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Driver Code */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        // Initialize double-ended queue\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    public int[] toArray() {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    int[] nums;  // Array for storing double-ended queue elements\n    int front;   // Front pointer, points to the front of the queue element\n    int queSize; // Double-ended queue length\n\n    /* Constructor */\n    public ArrayDeque(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    int Index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + Capacity()) % Capacity();\n    }\n\n    /* Front of the queue enqueue */\n    public void PushFirst(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = Index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    public void PushLast(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = Index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    public int PopFirst() {\n        int num = PeekFirst();\n        // Move front pointer backward by one position\n        front = Index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    public int PopLast() {\n        int num = PeekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int PeekFirst() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        return nums[front];\n    }\n\n    /* Driver Code */\n    public int PeekLast() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        // Initialize double-ended queue\n        int last = Index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    public int[] ToArray() {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\ntype arrayDeque struct {\n    nums        []int // Array for storing double-ended queue elements\n    front       int   // Front pointer, points to the front of the queue element\n    queSize     int   // Double-ended queue length\n    queCapacity int   // Queue capacity (maximum number of elements)\n}\n\n/* Access front of the queue element */\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/* Get the length of the double-ended queue */\nfunc (q *arrayDeque) size() int {\n    return q.queSize\n}\n\n/* Check if the double-ended queue is empty */\nfunc (q *arrayDeque) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Calculate circular array index */\nfunc (q *arrayDeque) index(i int) int {\n    // Use modulo operation to wrap the array head and tail together\n    // When i passes the tail of the array, return to the head\n    // When i passes the head of the array, return to the tail\n    return (i + q.queCapacity) % q.queCapacity\n}\n\n/* Front of the queue enqueue */\nfunc (q *arrayDeque) pushFirst(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Double-ended queue is full\")\n        return\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Add num to the front of the queue\n    q.front = q.index(q.front - 1)\n    // Add num to front of queue\n    q.nums[q.front] = num\n    q.queSize++\n}\n\n/* Rear of the queue enqueue */\nfunc (q *arrayDeque) pushLast(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Double-ended queue is full\")\n        return\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    rear := q.index(q.front + q.queSize)\n    // Front pointer moves one position backward\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Rear of the queue dequeue */\nfunc (q *arrayDeque) popFirst() any {\n    num := q.peekFirst()\n    if num == nil {\n        return nil\n    }\n    // Move front pointer backward by one position\n    q.front = q.index(q.front + 1)\n    q.queSize--\n    return num\n}\n\n/* Access rear of the queue element */\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/* Return list for printing */\nfunc (q *arrayDeque) peekFirst() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Driver Code */\nfunc (q *arrayDeque) peekLast() any {\n    if q.isEmpty() {\n        return nil\n    }\n    // Initialize double-ended queue\n    last := q.index(q.front + q.queSize - 1)\n    return q.nums[last]\n}\n\n/* Get Slice for printing */\nfunc (q *arrayDeque) toSlice() []int {\n    // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private var nums: [Int] // Array for storing double-ended queue elements\n    private var front: Int // Front pointer, points to the front of the queue element\n    private var _size: Int // Double-ended queue length\n\n    /* Constructor */\n    init(capacity: Int) {\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Get the capacity of the double-ended queue */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Get the length of the double-ended queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the double-ended queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Calculate circular array index */\n    private func index(i: Int) -> Int {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        (i + capacity()) % capacity()\n    }\n\n    /* Front of the queue enqueue */\n    func pushFirst(num: Int) {\n        if size() == capacity() {\n            print(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(i: front - 1)\n        // Add num to front of queue\n        nums[front] = num\n        _size += 1\n    }\n\n    /* Rear of the queue enqueue */\n    func pushLast(num: Int) {\n        if size() == capacity() {\n            print(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        let rear = index(i: front + size())\n        // Front pointer moves one position backward\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Rear of the queue dequeue */\n    func popFirst() -> Int {\n        let num = peekFirst()\n        // Move front pointer backward by one position\n        front = index(i: front + 1)\n        _size -= 1\n        return num\n    }\n\n    /* Access rear of the queue element */\n    func popLast() -> Int {\n        let num = peekLast()\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return nums[front]\n    }\n\n    /* Driver Code */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        // Initialize double-ended queue\n        let last = index(i: front + size() - 1)\n        return nums[last]\n    }\n\n    /* Return array for printing */\n    func toArray() -> [Int] {\n        // Elements enqueue\n        (front ..< front + size()).map { nums[index(i: $0)] }\n    }\n}\n
    array_deque.js
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    #nums; // Array for storing double-ended queue elements\n    #front; // Front pointer, points to the front of the queue element\n    #queSize; // Double-ended queue length\n\n    /* Constructor */\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n        this.#front = 0;\n        this.#queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    capacity() {\n        return this.#nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Calculate circular array index */\n    index(i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Front of the queue enqueue */\n    pushFirst(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        this.#front = this.index(this.#front - 1);\n        // Add num to front of queue\n        this.#nums[this.#front] = num;\n        this.#queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    pushLast(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        const rear = this.index(this.#front + this.#queSize);\n        // Front pointer moves one position backward\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    popFirst() {\n        const num = this.peekFirst();\n        // Move front pointer backward by one position\n        this.#front = this.index(this.#front + 1);\n        this.#queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    popLast() {\n        const num = this.peekLast();\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peekFirst() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.#nums[this.#front];\n    }\n\n    /* Driver Code */\n    peekLast() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Initialize double-ended queue\n        const last = this.index(this.#front + this.#queSize - 1);\n        return this.#nums[last];\n    }\n\n    /* Return array for printing */\n    toArray() {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private nums: number[]; // Array for storing double-ended queue elements\n    private front: number; // Front pointer, points to the front of the queue element\n    private queSize: number; // Double-ended queue length\n\n    /* Constructor */\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = 0;\n        this.queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Calculate circular array index */\n    index(i: number): number {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Front of the queue enqueue */\n    pushFirst(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        this.front = this.index(this.front - 1);\n        // Add num to front of queue\n        this.nums[this.front] = num;\n        this.queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    pushLast(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        const rear: number = this.index(this.front + this.queSize);\n        // Front pointer moves one position backward\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    popFirst(): number {\n        const num: number = this.peekFirst();\n        // Move front pointer backward by one position\n        this.front = this.index(this.front + 1);\n        this.queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    popLast(): number {\n        const num: number = this.peekLast();\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peekFirst(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.nums[this.front];\n    }\n\n    /* Driver Code */\n    peekLast(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Initialize double-ended queue\n        const last = this.index(this.front + this.queSize - 1);\n        return this.nums[last];\n    }\n\n    /* Return array for printing */\n    toArray(): number[] {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n  late List<int> _nums; // Array for storing double-ended queue elements\n  late int _front; // Front pointer, points to the front of the queue element\n  late int _queSize; // Double-ended queue length\n\n  /* Constructor */\n  ArrayDeque(int capacity) {\n    this._nums = List.filled(capacity, 0);\n    this._front = this._queSize = 0;\n  }\n\n  /* Get the capacity of the double-ended queue */\n  int capacity() {\n    return _nums.length;\n  }\n\n  /* Get the length of the double-ended queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the double-ended queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Calculate circular array index */\n  int index(int i) {\n    // Use modulo operation to wrap the array head and tail together\n    // When i passes the tail of the array, return to the head\n    // When i passes the head of the array, return to the tail\n    return (i + capacity()) % capacity();\n  }\n\n  /* Front of the queue enqueue */\n  void pushFirst(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Double-ended queue is full\");\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Use modulo operation to wrap _front from array head back to tail\n    _front = index(_front - 1);\n    // Add _num to queue front\n    _nums[_front] = _num;\n    _queSize++;\n  }\n\n  /* Rear of the queue enqueue */\n  void pushLast(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Double-ended queue is full\");\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    int rear = index(_front + _queSize);\n    // Add _num to queue rear\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Rear of the queue dequeue */\n  int popFirst() {\n    int _num = peekFirst();\n    // Move front pointer right by one\n    _front = index(_front + 1);\n    _queSize--;\n    return _num;\n  }\n\n  /* Access rear of the queue element */\n  int popLast() {\n    int _num = peekLast();\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peekFirst() {\n    if (isEmpty()) {\n      throw Exception(\"Deque is empty\");\n    }\n    return _nums[_front];\n  }\n\n  /* Driver Code */\n  int peekLast() {\n    if (isEmpty()) {\n      throw Exception(\"Deque is empty\");\n    }\n    // Initialize double-ended queue\n    int last = index(_front + _queSize - 1);\n    return _nums[last];\n  }\n\n  /* Return array for printing */\n  List<int> toArray() {\n    // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\nstruct ArrayDeque<T> {\n    nums: Vec<T>,    // Array for storing double-ended queue elements\n    front: usize,    // Front pointer, points to the front of the queue element\n    que_size: usize, // Double-ended queue length\n}\n\nimpl<T: Copy + Default> ArrayDeque<T> {\n    /* Constructor */\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    /* Get the capacity of the double-ended queue */\n    pub fn capacity(&self) -> usize {\n        self.nums.len()\n    }\n\n    /* Get the length of the double-ended queue */\n    pub fn size(&self) -> usize {\n        self.que_size\n    }\n\n    /* Check if the double-ended queue is empty */\n    pub fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Calculate circular array index */\n    fn index(&self, i: i32) -> usize {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n    }\n\n    /* Front of the queue enqueue */\n    pub fn push_first(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        self.front = self.index(self.front as i32 - 1);\n        // Add num to front of queue\n        self.nums[self.front] = num;\n        self.que_size += 1;\n    }\n\n    /* Rear of the queue enqueue */\n    pub fn push_last(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        let rear = self.index(self.front as i32 + self.que_size as i32);\n        // Front pointer moves one position backward\n        self.nums[rear] = num;\n        self.que_size += 1;\n    }\n\n    /* Rear of the queue dequeue */\n    fn pop_first(&mut self) -> T {\n        let num = self.peek_first();\n        // Move front pointer backward by one position\n        self.front = self.index(self.front as i32 + 1);\n        self.que_size -= 1;\n        num\n    }\n\n    /* Access rear of the queue element */\n    fn pop_last(&mut self) -> T {\n        let num = self.peek_last();\n        self.que_size -= 1;\n        num\n    }\n\n    /* Return list for printing */\n    fn peek_first(&self) -> T {\n        if self.is_empty() {\n            panic!(\"Deque is empty\")\n        };\n        self.nums[self.front]\n    }\n\n    /* Driver Code */\n    fn peek_last(&self) -> T {\n        if self.is_empty() {\n            panic!(\"Deque is empty\")\n        };\n        // Initialize double-ended queue\n        let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n        self.nums[last]\n    }\n\n    /* Return array for printing */\n    fn to_array(&self) -> Vec<T> {\n        // Elements enqueue\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
    /* Double-ended queue based on circular array implementation */\ntypedef struct {\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Rear pointer, points to rear + 1\n    int queCapacity; // Queue capacity\n} ArrayDeque;\n\n/* Constructor */\nArrayDeque *newArrayDeque(int capacity) {\n    ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n    // Initialize array\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/* Destructor */\nvoid delArrayDeque(ArrayDeque *deque) {\n    free(deque->nums);\n    free(deque);\n}\n\n/* Get the capacity of the double-ended queue */\nint capacity(ArrayDeque *deque) {\n    return deque->queCapacity;\n}\n\n/* Get the length of the double-ended queue */\nint size(ArrayDeque *deque) {\n    return deque->queSize;\n}\n\n/* Check if the double-ended queue is empty */\nbool empty(ArrayDeque *deque) {\n    return deque->queSize == 0;\n}\n\n/* Calculate circular array index */\nint dequeIndex(ArrayDeque *deque, int i) {\n    // Use modulo operation to wrap the array head and tail together\n    // When i exceeds array end, wrap to head\n    // When i passes the head of the array, return to the tail\n    return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* Front of the queue enqueue */\nvoid pushFirst(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Deque is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Use modulo to wrap front from array head to rear\n    deque->front = dequeIndex(deque, deque->front - 1);\n    // Add num to queue front\n    deque->nums[deque->front] = num;\n    deque->queSize++;\n}\n\n/* Rear of the queue enqueue */\nvoid pushLast(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Deque is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    int rear = dequeIndex(deque, deque->front + deque->queSize);\n    // Front pointer moves one position backward\n    deque->nums[rear] = num;\n    deque->queSize++;\n}\n\n/* Return list for printing */\nint peekFirst(ArrayDeque *deque) {\n    // Access error: Deque is empty\n    assert(empty(deque) == 0);\n    return deque->nums[deque->front];\n}\n\n/* Driver Code */\nint peekLast(ArrayDeque *deque) {\n    // Access error: Deque is empty\n    assert(empty(deque) == 0);\n    int last = dequeIndex(deque, deque->front + deque->queSize - 1);\n    return deque->nums[last];\n}\n\n/* Rear of the queue dequeue */\nint popFirst(ArrayDeque *deque) {\n    int num = peekFirst(deque);\n    // Move front pointer backward by one position\n    deque->front = dequeIndex(deque, deque->front + 1);\n    deque->queSize--;\n    return num;\n}\n\n/* Access rear of the queue element */\nint popLast(ArrayDeque *deque) {\n    int num = peekLast(deque);\n    deque->queSize--;\n    return num;\n}\n\n/* Return array for printing */\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
    /* Constructor */\nclass ArrayDeque(capacity: Int) {\n    private var nums: IntArray = IntArray(capacity) // Array for storing double-ended queue elements\n    private var front: Int = 0 // Front pointer, points to the front of the queue element\n    private var queSize: Int = 0 // Double-ended queue length\n\n    /* Get the capacity of the double-ended queue */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Get the length of the double-ended queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the double-ended queue is empty */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Calculate circular array index */\n    private fun index(i: Int): Int {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity()\n    }\n\n    /* Front of the queue enqueue */\n    fun pushFirst(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1)\n        // Add num to front of queue\n        nums[front] = num\n        queSize++\n    }\n\n    /* Rear of the queue enqueue */\n    fun pushLast(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        val rear = index(front + queSize)\n        // Front pointer moves one position backward\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Rear of the queue dequeue */\n    fun popFirst(): Int {\n        val num = peekFirst()\n        // Move front pointer backward by one position\n        front = index(front + 1)\n        queSize--\n        return num\n    }\n\n    /* Access rear of the queue element */\n    fun popLast(): Int {\n        val num = peekLast()\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Driver Code */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Initialize double-ended queue\n        val last = index(front + queSize - 1)\n        return nums[last]\n    }\n\n    /* Return array for printing */\n    fun toArray(): IntArray {\n        // Elements enqueue\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
    ### Deque based on circular array ###\nclass ArrayDeque\n  ### Get deque length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize(capacity)\n    @nums = Array.new(capacity, 0)\n    @front = 0\n    @size = 0\n  end\n\n  ### Get deque capacity ###\n  def capacity\n    @nums.length\n  end\n\n  ### Check if deque is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue at front ###\n  def push_first(num)\n    if size == capacity\n      puts 'Double-ended queue is full'\n      return\n    end\n\n    # Use modulo operation to wrap front around to the tail after passing the head of the array\n    # Add num to the front of the queue\n    @front = index(@front - 1)\n    # Add num to front of queue\n    @nums[@front] = num\n    @size += 1\n  end\n\n  ### Enqueue at rear ###\n  def push_last(num)\n    if size == capacity\n      puts 'Double-ended queue is full'\n      return\n    end\n\n    # Use modulo operation to wrap rear around to the head after passing the tail of the array\n    rear = index(@front + size)\n    # Front pointer moves one position backward\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Dequeue from front ###\n  def pop_first\n    num = peek_first\n    # Move front pointer backward by one position\n    @front = index(@front + 1)\n    @size -= 1\n    num\n  end\n\n  ### Dequeue from rear ###\n  def pop_last\n    num = peek_last\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek_first\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Access rear element ###\n  def peek_last\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    # Initialize double-ended queue\n    last = index(@front + size - 1)\n    @nums[last]\n  end\n\n  ### Return array for printing ###\n  def to_array\n    # Elements enqueue\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  ### Calculate circular array index ###\n  def index(i)\n    # Use modulo operation to wrap the array head and tail together\n    # When i passes the tail of the array, return to the head\n    # When i passes the head of the array, return to the tail\n    (i + capacity) % capacity\n  end\nend\n
    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533-deque-applications","level":2,"title":"5.3.3   Deque Applications","text":"

    A deque combines the logic of both stacks and queues. Therefore, it can implement all application scenarios of both, while providing greater flexibility.

    We know that the \"undo\" function in software is typically implemented using a stack: the system pushes each change operation onto the stack and then implements undo through pop. However, considering system resource limitations, software usually limits the number of undo steps (for example, only allowing 50 steps to be saved). When the stack length exceeds 50, the software needs to perform a deletion operation at the bottom of the stack (front of the queue). But a stack cannot implement this functionality, so a deque is needed to replace the stack. Note that the core logic of \"undo\" still follows the LIFO principle of a stack; it's just that the deque can more flexibly implement some additional logic.

    ","path":["Chapter 5. Stacks and Queues","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   Queue","text":"

    A queue is a linear data structure that follows the First In, First Out (FIFO) rule. As the name suggests, it models people lining up: newcomers continuously join the rear of the queue, while the people at the front leave one by one.

    As shown in Figure 5-4, we call the front of the queue the \"front\" and the end the \"rear.\" The operation of adding an element to the rear is called \"enqueue,\" and the operation of removing the front element is called \"dequeue.\"

    Figure 5-4   FIFO rule of queue

    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521-common-queue-operations","level":2,"title":"5.2.1   Common Queue Operations","text":"

    The common operations on a queue are shown in Table 5-2. Note that method names may vary across programming languages. Here, we use the same naming convention as for stacks.

    Table 5-2   Efficiency of Queue Operations

    Method Description Time Complexity push() Enqueue element, add element to rear \\(O(1)\\) pop() Dequeue front element \\(O(1)\\) peek() Access front element \\(O(1)\\)

    We can directly use the queue classes provided by the programming language:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py
    from collections import deque\n\n# Initialize queue\n# In Python, we generally use the deque class as a queue\n# Although queue.Queue() is a pure queue class, it is not very user-friendly, so it is not recommended\nque: deque[int] = deque()\n\n# Enqueue elements\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# Access front element\nfront: int = que[0]\n\n# Dequeue element\npop: int = que.popleft()\n\n# Get queue length\nsize: int = len(que)\n\n# Check if queue is empty\nis_empty: bool = len(que) == 0\n
    queue.cpp
    /* Initialize queue */\nqueue<int> queue;\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nint front = queue.front();\n\n/* Dequeue element */\nqueue.pop();\n\n/* Get queue length */\nint size = queue.size();\n\n/* Check if queue is empty */\nbool empty = queue.empty();\n
    queue.java
    /* Initialize queue */\nQueue<Integer> queue = new LinkedList<>();\n\n/* Enqueue elements */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* Access front element */\nint peek = queue.peek();\n\n/* Dequeue element */\nint pop = queue.poll();\n\n/* Get queue length */\nint size = queue.size();\n\n/* Check if queue is empty */\nboolean isEmpty = queue.isEmpty();\n
    queue.cs
    /* Initialize queue */\nQueue<int> queue = new();\n\n/* Enqueue elements */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* Access front element */\nint peek = queue.Peek();\n\n/* Dequeue element */\nint pop = queue.Dequeue();\n\n/* Get queue length */\nint size = queue.Count;\n\n/* Check if queue is empty */\nbool isEmpty = queue.Count == 0;\n
    queue_test.go
    /* Initialize queue */\n// In Go, use list as a queue\nqueue := list.New()\n\n/* Enqueue elements */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* Access front element */\npeek := queue.Front()\n\n/* Dequeue element */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* Get queue length */\nsize := queue.Len()\n\n/* Check if queue is empty */\nisEmpty := queue.Len() == 0\n
    queue.swift
    /* Initialize queue */\n// Swift does not have a built-in queue class, can use Array as a queue\nvar queue: [Int] = []\n\n/* Enqueue elements */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* Access front element */\nlet peek = queue.first!\n\n/* Dequeue element */\n// Since it's an array, removeFirst has O(n) complexity\nlet pool = queue.removeFirst()\n\n/* Get queue length */\nlet size = queue.count\n\n/* Check if queue is empty */\nlet isEmpty = queue.isEmpty\n
    queue.js
    /* Initialize queue */\n// JavaScript does not have a built-in queue, can use Array as a queue\nconst queue = [];\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nconst peek = queue[0];\n\n/* Dequeue element */\n// The underlying structure is an array, so shift() has O(n) time complexity\nconst pop = queue.shift();\n\n/* Get queue length */\nconst size = queue.length;\n\n/* Check if queue is empty */\nconst empty = queue.length === 0;\n
    queue.ts
    /* Initialize queue */\n// TypeScript does not have a built-in queue, can use Array as a queue\nconst queue: number[] = [];\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nconst peek = queue[0];\n\n/* Dequeue element */\n// The underlying structure is an array, so shift() has O(n) time complexity\nconst pop = queue.shift();\n\n/* Get queue length */\nconst size = queue.length;\n\n/* Check if queue is empty */\nconst empty = queue.length === 0;\n
    queue.dart
    /* Initialize queue */\n// In Dart, the Queue class is a deque and can also be used as a queue\nQueue<int> queue = Queue();\n\n/* Enqueue elements */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* Access front element */\nint peek = queue.first;\n\n/* Dequeue element */\nint pop = queue.removeFirst();\n\n/* Get queue length */\nint size = queue.length;\n\n/* Check if queue is empty */\nbool isEmpty = queue.isEmpty;\n
    queue.rs
    /* Initialize deque */\n// In Rust, use deque as a regular queue\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Enqueue elements */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* Access front element */\nif let Some(front) = deque.front() {\n}\n\n/* Dequeue element */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* Get queue length */\nlet size = deque.len();\n\n/* Check if queue is empty */\nlet is_empty = deque.is_empty();\n
    queue.c
    // C does not provide a built-in queue\n
    queue.kt
    /* Initialize queue */\nval queue = LinkedList<Int>()\n\n/* Enqueue elements */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* Access front element */\nval peek = queue.peek()\n\n/* Dequeue element */\nval pop = queue.poll()\n\n/* Get queue length */\nval size = queue.size\n\n/* Check if queue is empty */\nval isEmpty = queue.isEmpty()\n
    queue.rb
    # Initialize queue\n# Ruby's built-in queue (Thread::Queue) does not have peek and traversal methods, can use Array as a queue\nqueue = []\n\n# Enqueue elements\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# Access front element\npeek = queue.first\n\n# Dequeue element\n# Please note that since it's an array, Array#shift has O(n) time complexity\npop = queue.shift\n\n# Get queue length\nsize = queue.length\n\n# Check if queue is empty\nis_empty = queue.empty?\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522-queue-implementation","level":2,"title":"5.2.2   Queue Implementation","text":"

    To implement a queue, we need a data structure that allows adding elements at one end and removing elements at the other end. Both linked lists and arrays meet this requirement.

    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1-linked-list-implementation","level":3,"title":"1.   Linked List Implementation","text":"

    As shown in Figure 5-5, we can treat the \"head node\" and \"tail node\" of a linked list as the \"front\" and \"rear\" of the queue, respectively, with the rule that nodes can only be added at the rear and removed from the front.

    <1><2><3>

    Figure 5-5   Enqueue and dequeue operations in linked list implementation of queue

    Below is the code for implementing a queue using a linked list:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py
    class LinkedListQueue:\n    \"\"\"Queue based on linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._front: ListNode | None = None  # Head node front\n        self._rear: ListNode | None = None  # Tail node rear\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Get the length of the queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Enqueue\"\"\"\n        # Add num after the tail node\n        node = ListNode(num)\n        # If the queue is empty, make both front and rear point to the node\n        if self._front is None:\n            self._front = node\n            self._rear = node\n        # If the queue is not empty, add the node after the tail node\n        else:\n            self._rear.next = node\n            self._rear = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Dequeue\"\"\"\n        num = self.peek()\n        # Delete head node\n        self._front = self._front.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Queue is empty\")\n        return self._front.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Convert to list for printing\"\"\"\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n  private:\n    ListNode *front, *rear; // Head node front, tail node rear\n    int queSize;\n\n  public:\n    LinkedListQueue() {\n        front = nullptr;\n        rear = nullptr;\n        queSize = 0;\n    }\n\n    ~LinkedListQueue() {\n        // Traverse linked list to delete nodes and free memory\n        freeMemoryLinkedList(front);\n    }\n\n    /* Get the length of the queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    void push(int num) {\n        // Add num after the tail node\n        ListNode *node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == nullptr) {\n            front = node;\n            rear = node;\n        }\n        // If the queue is not empty, add the node after the tail node\n        else {\n            rear->next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    int pop() {\n        int num = peek();\n        // Delete head node\n        ListNode *tmp = front;\n        front = front->next;\n        // Free memory\n        delete tmp;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peek() {\n        if (size() == 0)\n            throw out_of_range(\"Queue is empty\");\n        return front->val;\n    }\n\n    /* Convert linked list to Vector and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private ListNode front, rear; // Head node front, tail node rear\n    private int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue */\n    public void push(int num) {\n        // Add num after the tail node\n        ListNode node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node;\n            rear = node;\n        // If the queue is not empty, add the node after the tail node\n        } else {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int pop() {\n        int num = peek();\n        // Delete head node\n        front = front.next;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    ListNode? front, rear;  // Head node front, tail node rear\n    int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Enqueue */\n    public void Push(int num) {\n        // Add num after the tail node\n        ListNode node = new(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node;\n            rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else if (rear != null) {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int Pop() {\n        int num = Peek();\n        // Delete head node\n        front = front?.next;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return front!.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\ntype linkedListQueue struct {\n    // Use built-in package list to implement queue\n    data *list.List\n}\n\n/* Access front of the queue element */\nfunc newLinkedListQueue() *linkedListQueue {\n    return &linkedListQueue{\n        data: list.New(),\n    }\n}\n\n/* Enqueue */\nfunc (s *linkedListQueue) push(value any) {\n    s.data.PushBack(value)\n}\n\n/* Dequeue */\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/* Return list for printing */\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/* Get the length of the queue */\nfunc (s *linkedListQueue) size() int {\n    return s.data.Len()\n}\n\n/* Check if the queue is empty */\nfunc (s *linkedListQueue) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListQueue) toList() *list.List {\n    return s.data\n}\n
    linkedlist_queue.swift
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private var front: ListNode? // Head node\n    private var rear: ListNode? // Tail node\n    private var _size: Int\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue */\n    func push(num: Int) {\n        // Add num after the tail node\n        let node = ListNode(x: num)\n        // If the queue is empty, make both front and rear point to the node\n        if front == nil {\n            front = node\n            rear = node\n        }\n        // If the queue is not empty, add the node after the tail node\n        else {\n            rear?.next = node\n            rear = node\n        }\n        _size += 1\n    }\n\n    /* Dequeue */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Delete head node\n        front = front?.next\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Queue is empty\")\n        }\n        return front!.val\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    #front; // Front node #front\n    #rear; // Rear node #rear\n    #queSize = 0;\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n    }\n\n    /* Get the length of the queue */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Enqueue */\n    push(num) {\n        // Add num after the tail node\n        const node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (!this.#front) {\n            this.#front = node;\n            this.#rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            this.#rear.next = node;\n            this.#rear = node;\n        }\n        this.#queSize++;\n    }\n\n    /* Dequeue */\n    pop() {\n        const num = this.peek();\n        // Delete head node\n        this.#front = this.#front.next;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (this.size === 0) throw new Error('Queue is empty');\n        return this.#front.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private front: ListNode | null; // Head node front\n    private rear: ListNode | null; // Tail node rear\n    private queSize: number = 0;\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n    }\n\n    /* Get the length of the queue */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Enqueue */\n    push(num: number): void {\n        // Add num after the tail node\n        const node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (!this.front) {\n            this.front = node;\n            this.rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            this.rear!.next = node;\n            this.rear = node;\n        }\n        this.queSize++;\n    }\n\n    /* Dequeue */\n    pop(): number {\n        const num = this.peek();\n        if (!this.front) throw new Error('Queue is empty');\n        // Delete head node\n        this.front = this.front.next;\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (this.size === 0) throw new Error('Queue is empty');\n        return this.front!.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue {\n  ListNode? _front; // Head node _front\n  ListNode? _rear; // Tail node _rear\n  int _queSize = 0; // Queue length\n\n  LinkedListQueue() {\n    _front = null;\n    _rear = null;\n  }\n\n  /* Get the length of the queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Enqueue */\n  void push(int _num) {\n    // Add _num after tail node\n    final node = ListNode(_num);\n    // If the queue is empty, make both front and rear point to the node\n    if (_front == null) {\n      _front = node;\n      _rear = node;\n    } else {\n      // If the queue is not empty, add the node after the tail node\n      _rear!.next = node;\n      _rear = node;\n    }\n    _queSize++;\n  }\n\n  /* Dequeue */\n  int pop() {\n    final int _num = peek();\n    // Delete head node\n    _front = _front!.next;\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (_queSize == 0) {\n      throw Exception('Queue is empty');\n    }\n    return _front!.val;\n  }\n\n  /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListQueue<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Head node front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Tail node rear\n    que_size: usize,                         // Queue length\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    /* Get the length of the queue */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Check if the queue is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Enqueue */\n    pub fn push(&mut self, num: T) {\n        // Add num after the tail node\n        let new_rear = ListNode::new(num);\n        match self.rear.take() {\n            // If the queue is not empty, add the node after the tail node\n            Some(old_rear) => {\n                old_rear.borrow_mut().next = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n            // If the queue is empty, make both front and rear point to the node\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    /* Dequeue */\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    /* Return list for printing */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Queue based on linked list implementation */\ntypedef struct {\n    ListNode *front, *rear;\n    int queSize;\n} LinkedListQueue;\n\n/* Constructor */\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/* Destructor */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n    // Free all nodes\n    while (queue->front != NULL) {\n        ListNode *tmp = queue->front;\n        queue->front = queue->front->next;\n        free(tmp);\n    }\n    // Free queue structure\n    free(queue);\n}\n\n/* Get the length of the queue */\nint size(LinkedListQueue *queue) {\n    return queue->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(LinkedListQueue *queue) {\n    return (size(queue) == 0);\n}\n\n/* Enqueue */\nvoid push(LinkedListQueue *queue, int num) {\n    // Add node at tail\n    ListNode *node = newListNode(num);\n    // If the queue is empty, make both front and rear point to the node\n    if (queue->front == NULL) {\n        queue->front = node;\n        queue->rear = node;\n    }\n    // If the queue is not empty, add the node after the tail node\n    else {\n        queue->rear->next = node;\n        queue->rear = node;\n    }\n    queue->queSize++;\n}\n\n/* Return list for printing */\nint peek(LinkedListQueue *queue) {\n    assert(size(queue) && queue->front);\n    return queue->front->val;\n}\n\n/* Dequeue */\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/* Print queue */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n    int *arr = malloc(sizeof(int) * queue->queSize);\n    // Copy data from list to array\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
    /* Queue based on linked list implementation */\nclass LinkedListQueue(\n    // Head node front, tail node rear\n    private var front: ListNode? = null,\n    private var rear: ListNode? = null,\n    private var queSize: Int = 0\n) {\n\n    /* Get the length of the queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the queue is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Enqueue */\n    fun push(num: Int) {\n        // Add num after the tail node\n        val node = ListNode(num)\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node\n            rear = node\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            rear?.next = node\n            rear = node\n        }\n        queSize++\n    }\n\n    /* Dequeue */\n    fun pop(): Int {\n        val num = peek()\n        // Delete head node\n        front = front?.next\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Convert linked list to Array and return */\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
    ### Queue based on linked list ###\nclass LinkedListQueue\n  ### Get queue length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @front = nil  # Head node front\n    @rear = nil   # Tail node rear\n    @size = 0\n  end\n\n  ### Check if queue is empty ###\n  def is_empty?\n    @front.nil?\n  end\n\n  ### Enqueue ###\n  def push(num)\n    # Add num after the tail node\n    node = ListNode.new(num)\n\n    # If queue is empty, set both front and rear to this node\n    if @front.nil?\n      @front = node\n      @rear = node\n    # If queue is not empty, add this node after rear\n    else\n      @rear.next = node\n      @rear = node\n    end\n\n    @size += 1\n  end\n\n  ### Dequeue ###\n  def pop\n    num = peek\n    # Delete head node\n    @front = @front.next\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek\n    raise IndexError, 'Queue is empty' if is_empty?\n\n    @front.val\n  end\n\n  ### Convert linked list to Array and return ###\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":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

    Deleting the first element in an array has a time complexity of \\(O(n)\\), which would make the dequeue operation inefficient. However, we can use the following clever method to avoid this problem.

    We can use a variable front to point to the index of the front element and maintain a variable size to record the queue length. We define rear = front + size, which calculates the position right after the rear element.

    Based on this design, the valid interval containing elements in the array is [front, rear - 1]. The implementation methods for various operations are shown in Figure 5-6:

    • Enqueue operation: Assign the input element to the rear index and increase size by 1.
    • Dequeue operation: Simply increase front by 1 and decrease size by 1.

    As you can see, both enqueue and dequeue operations require only one operation, with a time complexity of \\(O(1)\\).

    <1><2><3>

    Figure 5-6   Enqueue and dequeue operations in array implementation of queue

    You may notice a problem: as we continuously enqueue and dequeue, both front and rear move to the right. When they reach the end of the array, they cannot continue moving. To solve this problem, we can treat the array as a \"circular array\" with head and tail connected.

    For a circular array, we need to let front or rear wrap around to the beginning of the array when they cross the end. This periodic pattern can be implemented using the \"modulo operation,\" as shown in the code below:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py
    class ArrayQueue:\n    \"\"\"Queue based on circular array implementation\"\"\"\n\n    def __init__(self, size: int):\n        \"\"\"Constructor\"\"\"\n        self._nums: list[int] = [0] * size  # Array for storing queue elements\n        self._front: int = 0  # Front pointer, points to the front of the queue element\n        self._size: int = 0  # Queue length\n\n    def capacity(self) -> int:\n        \"\"\"Get the capacity of the queue\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Get the length of the queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Enqueue\"\"\"\n        if self._size == self.capacity():\n            raise IndexError(\"Queue is full\")\n        # Calculate rear pointer, points to rear index + 1\n        # Use modulo operation to wrap rear around to the head after passing the tail of the array\n        rear: int = (self._front + self._size) % self.capacity()\n        # Add num to the rear of the queue\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Dequeue\"\"\"\n        num: int = self.peek()\n        # Front pointer moves one position backward, if it passes the tail, return to the head of the array\n        self._front = (self._front + 1) % self.capacity()\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Queue is empty\")\n        return self._nums[self._front]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Return list for printing\"\"\"\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n  private:\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Queue length\n    int queCapacity; // Queue capacity\n\n  public:\n    ArrayQueue(int capacity) {\n        // Initialize array\n        nums = new int[capacity];\n        queCapacity = capacity;\n        front = queSize = 0;\n    }\n\n    ~ArrayQueue() {\n        delete[] nums;\n    }\n\n    /* Get the capacity of the queue */\n    int capacity() {\n        return queCapacity;\n    }\n\n    /* Get the length of the queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue */\n    void push(int num) {\n        if (queSize == queCapacity) {\n            cout << \"Queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % queCapacity;\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    int pop() {\n        int num = peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % queCapacity;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peek() {\n        if (isEmpty())\n            throw out_of_range(\"Queue is empty\");\n        return nums[front];\n    }\n\n    /* Convert array to Vector and return */\n    vector<int> toVector() {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    private int[] nums; // Array for storing queue elements\n    private int front; // Front pointer, points to the front of the queue element\n    private int queSize; // Queue length\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Get the length of the queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    public void push(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % capacity();\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int pop() {\n        int num = peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Return array */\n    public int[] toArray() {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    int[] nums;  // Array for storing queue elements\n    int front;   // Front pointer, points to the front of the queue element\n    int queSize; // Queue length\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Get the length of the queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    public void Push(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % Capacity();\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int Pop() {\n        int num = Peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % Capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return nums[front];\n    }\n\n    /* Return array */\n    public int[] ToArray() {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\ntype arrayQueue struct {\n    nums        []int // Array for storing queue elements\n    front       int   // Front pointer, points to the front of the queue element\n    queSize     int   // Queue length\n    queCapacity int   // Queue capacity (maximum number of elements)\n}\n\n/* Access front of the queue element */\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/* Get the length of the queue */\nfunc (q *arrayQueue) size() int {\n    return q.queSize\n}\n\n/* Check if the queue is empty */\nfunc (q *arrayQueue) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Enqueue */\nfunc (q *arrayQueue) push(num int) {\n    // When rear == queCapacity, queue is full\n    if q.queSize == q.queCapacity {\n        return\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    rear := (q.front + q.queSize) % q.queCapacity\n    // Front pointer moves one position backward\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Dequeue */\nfunc (q *arrayQueue) pop() any {\n    num := q.peek()\n    if num == nil {\n        return nil\n    }\n\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    q.front = (q.front + 1) % q.queCapacity\n    q.queSize--\n    return num\n}\n\n/* Return list for printing */\nfunc (q *arrayQueue) peek() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Get Slice for printing */\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    private var nums: [Int] // Array for storing queue elements\n    private var front: Int // Front pointer, points to the front of the queue element\n    private var _size: Int // Queue length\n\n    init(capacity: Int) {\n        // Initialize array\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Get the capacity of the queue */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Get the length of the queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue */\n    func push(num: Int) {\n        if size() == capacity() {\n            print(\"Queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        let rear = (front + size()) % capacity()\n        // Front pointer moves one position backward\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Dequeue */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity()\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Queue is empty\")\n        }\n        return nums[front]\n    }\n\n    /* Return array */\n    func toArray() -> [Int] {\n        // Elements enqueue\n        (front ..< front + size()).map { nums[$0 % capacity()] }\n    }\n}\n
    array_queue.js
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    #nums; // Array for storing queue elements\n    #front = 0; // Front pointer, points to the front of the queue element\n    #queSize = 0; // Queue length\n\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n    }\n\n    /* Get the capacity of the queue */\n    get capacity() {\n        return this.#nums.length;\n    }\n\n    /* Get the length of the queue */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Enqueue */\n    push(num) {\n        if (this.size === this.capacity) {\n            console.log('Queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        const rear = (this.#front + this.size) % this.capacity;\n        // Front pointer moves one position backward\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Dequeue */\n    pop() {\n        const num = this.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        this.#front = (this.#front + 1) % this.capacity;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (this.isEmpty()) throw new Error('Queue is empty');\n        return this.#nums[this.#front];\n    }\n\n    /* Return Array */\n    toArray() {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n    private nums: number[]; // Array for storing queue elements\n    private front: number; // Front pointer, points to the front of the queue element\n    private queSize: number; // Queue length\n\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = this.queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    get capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Get the length of the queue */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Enqueue */\n    push(num: number): void {\n        if (this.size === this.capacity) {\n            console.log('Queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        const rear = (this.front + this.queSize) % this.capacity;\n        // Front pointer moves one position backward\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Dequeue */\n    pop(): number {\n        const num = this.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        this.front = (this.front + 1) % this.capacity;\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (this.isEmpty()) throw new Error('Queue is empty');\n        return this.nums[this.front];\n    }\n\n    /* Return Array */\n    toArray(): number[] {\n        // Elements enqueue\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
    /* Queue based on circular array implementation */\nclass ArrayQueue {\n  late List<int> _nums; // Array for storing queue elements\n  late int _front; // Front pointer, points to the front of the queue element\n  late int _queSize; // Queue length\n\n  ArrayQueue(int capacity) {\n    _nums = List.filled(capacity, 0);\n    _front = _queSize = 0;\n  }\n\n  /* Get the capacity of the queue */\n  int capaCity() {\n    return _nums.length;\n  }\n\n  /* Get the length of the queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Enqueue */\n  void push(int _num) {\n    if (_queSize == capaCity()) {\n      throw Exception(\"Queue is full\");\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    int rear = (_front + _queSize) % capaCity();\n    // Add _num to queue rear\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Dequeue */\n  int pop() {\n    int _num = peek();\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    _front = (_front + 1) % capaCity();\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"Queue is empty\");\n    }\n    return _nums[_front];\n  }\n\n  /* Return Array */\n  List<int> toArray() {\n    // Elements enqueue\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
    /* Queue based on circular array implementation */\nstruct ArrayQueue<T> {\n    nums: Vec<T>,      // Array for storing queue elements\n    front: i32,        // Front pointer, points to the front of the queue element\n    que_size: i32,     // Queue length\n    que_capacity: i32, // Queue capacity\n}\n\nimpl<T: Copy + Default> ArrayQueue<T> {\n    /* Constructor */\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    /* Get the capacity of the queue */\n    fn capacity(&self) -> i32 {\n        self.que_capacity\n    }\n\n    /* Get the length of the queue */\n    fn size(&self) -> i32 {\n        self.que_size\n    }\n\n    /* Check if the queue is empty */\n    fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Enqueue */\n    fn push(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        let rear = (self.front + self.que_size) % self.que_capacity;\n        // Front pointer moves one position backward\n        self.nums[rear as usize] = num;\n        self.que_size += 1;\n    }\n\n    /* Dequeue */\n    fn pop(&mut self) -> T {\n        let num = self.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        self.front = (self.front + 1) % self.que_capacity;\n        self.que_size -= 1;\n        num\n    }\n\n    /* Return list for printing */\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    /* Return array */\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
    /* Queue based on circular array implementation */\ntypedef struct {\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Current number of elements in the queue\n    int queCapacity; // Queue capacity\n} ArrayQueue;\n\n/* Constructor */\nArrayQueue *newArrayQueue(int capacity) {\n    ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n    // Initialize array\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/* Destructor */\nvoid delArrayQueue(ArrayQueue *queue) {\n    free(queue->nums);\n    free(queue);\n}\n\n/* Get the capacity of the queue */\nint capacity(ArrayQueue *queue) {\n    return queue->queCapacity;\n}\n\n/* Get the length of the queue */\nint size(ArrayQueue *queue) {\n    return queue->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(ArrayQueue *queue) {\n    return queue->queSize == 0;\n}\n\n/* Return list for printing */\nint peek(ArrayQueue *queue) {\n    assert(size(queue) != 0);\n    return queue->nums[queue->front];\n}\n\n/* Enqueue */\nvoid push(ArrayQueue *queue, int num) {\n    if (size(queue) == capacity(queue)) {\n        printf(\"Queue is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    int rear = (queue->front + queue->queSize) % queue->queCapacity;\n    // Front pointer moves one position backward\n    queue->nums[rear] = num;\n    queue->queSize++;\n}\n\n/* Dequeue */\nint pop(ArrayQueue *queue) {\n    int num = peek(queue);\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    queue->front = (queue->front + 1) % queue->queCapacity;\n    queue->queSize--;\n    return num;\n}\n\n/* Return array for printing */\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
    /* Queue based on circular array implementation */\nclass ArrayQueue(capacity: Int) {\n    private val nums: IntArray = IntArray(capacity) // Array for storing queue elements\n    private var front: Int = 0 // Front pointer, points to the front of the queue element\n    private var queSize: Int = 0 // Queue length\n\n    /* Get the capacity of the queue */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Get the length of the queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the queue is empty */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Enqueue */\n    fun push(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        val rear = (front + queSize) % capacity()\n        // Front pointer moves one position backward\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Dequeue */\n    fun pop(): Int {\n        val num = peek()\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity()\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Return array */\n    fun toArray(): IntArray {\n        // Elements enqueue\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
    ### Queue based on circular array ###\nclass ArrayQueue\n  ### Get queue length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize(size)\n    @nums = Array.new(size, 0) # Array for storing queue elements\n    @front = 0 # Front pointer, points to the front of the queue element\n    @size = 0 # Queue length\n  end\n\n  ### Get queue capacity ###\n  def capacity\n    @nums.length\n  end\n\n  ### Check if queue is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue ###\n  def push(num)\n    raise IndexError, 'Queue is full' if size == capacity\n\n    # Use modulo operation to wrap rear around to the head after passing the tail of the array\n    # Add num to the rear of the queue\n    rear = (@front + size) % capacity\n    # Front pointer moves one position backward\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Dequeue ###\n  def pop\n    num = peek\n    # Move front pointer backward by one position, if it passes the tail, return to array head\n    @front = (@front + 1) % capacity\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek\n    raise IndexError, 'Queue is empty' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Return list for printing ###\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

    The queue implemented above still has limitations: its length is immutable. However, this problem is not difficult to solve. We can replace the array with a dynamic array to introduce an expansion mechanism. Interested readers can try to implement this themselves.

    The comparison conclusions for the two implementations are consistent with those for stacks and will not be repeated here.

    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523-typical-applications-of-queue","level":2,"title":"5.2.3   Typical Applications of Queue","text":"
    • Taobao orders. After shoppers place orders, the orders are added to a queue, and the system subsequently processes the orders in the queue according to their sequence. During Double Eleven, massive orders are generated in a short time, and high concurrency becomes a key challenge that engineers need to tackle.
    • Various to-do tasks. Any scenario that needs to implement \"first come, first served\" functionality, such as a printer's task queue or a restaurant's order queue, can effectively maintain the processing order using queues.
    ","path":["Chapter 5. Stacks and Queues","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   Stack","text":"

    A stack is a linear data structure that follows the Last In, First Out (LIFO) principle.

    We can compare a stack to a pile of plates on a table. If we specify that only one plate can be moved at a time, then to get the bottom plate, we must first remove the plates above it one by one. If we replace the plates with various types of elements (such as integers, characters, objects, etc.), we get the stack data structure.

    As shown in Figure 5-1, we call the top of the stacked elements the \"top\" and the bottom the \"bottom.\" The operation of adding an element to the top is called \"push,\" and the operation of removing the top element is called \"pop.\"

    Figure 5-1   LIFO rule of stack

    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511-common-stack-operations","level":2,"title":"5.1.1   Common Stack Operations","text":"

    The common operations on a stack are shown in Table 5-1. The specific method names depend on the programming language used. Here, we use the common naming convention of push(), pop(), and peek().

    Table 5-1   Efficiency of Stack Operations

    Method Description Time Complexity push() Push element onto stack (add to top) \\(O(1)\\) pop() Pop top element from stack \\(O(1)\\) peek() Access top element \\(O(1)\\)

    Typically, we can directly use the built-in stack class provided by the programming language. However, some languages may not provide a dedicated stack class. In such cases, we can use the language's \"array\" or \"linked list\" as a stack and simply avoid using operations unrelated to stack behavior.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py
    # Initialize stack\n# Python does not have a built-in stack class, can use list as a stack\nstack: list[int] = []\n\n# Push elements\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# Access top element\npeek: int = stack[-1]\n\n# Pop element\npop: int = stack.pop()\n\n# Get stack length\nsize: int = len(stack)\n\n# Check if empty\nis_empty: bool = len(stack) == 0\n
    stack.cpp
    /* Initialize stack */\nstack<int> stack;\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nint top = stack.top();\n\n/* Pop element */\nstack.pop(); // No return value\n\n/* Get stack length */\nint size = stack.size();\n\n/* Check if empty */\nbool empty = stack.empty();\n
    stack.java
    /* Initialize stack */\nStack<Integer> stack = new Stack<>();\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nint peek = stack.peek();\n\n/* Pop element */\nint pop = stack.pop();\n\n/* Get stack length */\nint size = stack.size();\n\n/* Check if empty */\nboolean isEmpty = stack.isEmpty();\n
    stack.cs
    /* Initialize stack */\nStack<int> stack = new();\n\n/* Push elements */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* Access top element */\nint peek = stack.Peek();\n\n/* Pop element */\nint pop = stack.Pop();\n\n/* Get stack length */\nint size = stack.Count;\n\n/* Check if empty */\nbool isEmpty = stack.Count == 0;\n
    stack_test.go
    /* Initialize stack */\n// In Go, it is recommended to use Slice as a stack\nvar stack []int\n\n/* Push elements */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* Access top element */\npeek := stack[len(stack)-1]\n\n/* Pop element */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* Get stack length */\nsize := len(stack)\n\n/* Check if empty */\nisEmpty := len(stack) == 0\n
    stack.swift
    /* Initialize stack */\n// Swift does not have a built-in stack class, can use Array as a stack\nvar stack: [Int] = []\n\n/* Push elements */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* Access top element */\nlet peek = stack.last!\n\n/* Pop element */\nlet pop = stack.removeLast()\n\n/* Get stack length */\nlet size = stack.count\n\n/* Check if empty */\nlet isEmpty = stack.isEmpty\n
    stack.js
    /* Initialize stack */\n// JavaScript does not have a built-in stack class, can use Array as a stack\nconst stack = [];\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nconst peek = stack[stack.length-1];\n\n/* Pop element */\nconst pop = stack.pop();\n\n/* Get stack length */\nconst size = stack.length;\n\n/* Check if empty */\nconst is_empty = stack.length === 0;\n
    stack.ts
    /* Initialize stack */\n// TypeScript does not have a built-in stack class, can use Array as a stack\nconst stack: number[] = [];\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nconst peek = stack[stack.length - 1];\n\n/* Pop element */\nconst pop = stack.pop();\n\n/* Get stack length */\nconst size = stack.length;\n\n/* Check if empty */\nconst is_empty = stack.length === 0;\n
    stack.dart
    /* Initialize stack */\n// Dart does not have a built-in stack class, can use List as a stack\nList<int> stack = [];\n\n/* Push elements */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* Access top element */\nint peek = stack.last;\n\n/* Pop element */\nint pop = stack.removeLast();\n\n/* Get stack length */\nint size = stack.length;\n\n/* Check if empty */\nbool isEmpty = stack.isEmpty;\n
    stack.rs
    /* Initialize stack */\n// Use Vec as a stack\nlet mut stack: Vec<i32> = Vec::new();\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nlet top = stack.last().unwrap();\n\n/* Pop element */\nlet pop = stack.pop().unwrap();\n\n/* Get stack length */\nlet size = stack.len();\n\n/* Check if empty */\nlet is_empty = stack.is_empty();\n
    stack.c
    // C does not provide a built-in stack\n
    stack.kt
    /* Initialize stack */\nval stack = Stack<Int>()\n\n/* Push elements */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* Access top element */\nval peek = stack.peek()\n\n/* Pop element */\nval pop = stack.pop()\n\n/* Get stack length */\nval size = stack.size\n\n/* Check if empty */\nval isEmpty = stack.isEmpty()\n
    stack.rb
    # Initialize stack\n# Ruby does not have a built-in stack class, can use Array as a stack\nstack = []\n\n# Push elements\nstack << 1\nstack << 3\nstack << 2\nstack << 5\nstack << 4\n\n# Access top element\npeek = stack.last\n\n# Pop element\npop = stack.pop\n\n# Get stack length\nsize = stack.length\n\n# Check if empty\nis_empty = stack.empty?\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512-stack-implementation","level":2,"title":"5.1.2   Stack Implementation","text":"

    To gain a deeper understanding of how a stack operates, let's try implementing a stack class ourselves.

    A stack follows the LIFO principle, so we can only add or remove elements at the top. However, both arrays and linked lists allow adding and removing elements at any position. Therefore, a stack can be viewed as a restricted array or linked list. In other words, we can \"shield\" some irrelevant operations of arrays or linked lists so that their external logic conforms to the characteristics of a stack.

    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1-linked-list-implementation","level":3,"title":"1.   Linked List Implementation","text":"

    When implementing a stack using a linked list, we can treat the head node of the linked list as the top of the stack and the tail node as the base.

    As shown in Figure 5-2, for the push operation, we simply insert an element at the head of the linked list. This node insertion method is called the \"head insertion method.\" For the pop operation, we just need to remove the head node from the linked list.

    <1><2><3>

    Figure 5-2   Push and pop operations in linked list implementation of stack

    Below is sample code for implementing a stack based on a linked list:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py
    class LinkedListStack:\n    \"\"\"Stack based on linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._peek: ListNode | None = None\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Get the length of the stack\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the stack is empty\"\"\"\n        return self._size == 0\n\n    def push(self, val: int):\n        \"\"\"Push\"\"\"\n        node = ListNode(val)\n        node.next = self._peek\n        self._peek = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Pop\"\"\"\n        num = self.peek()\n        self._peek = self._peek.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access top of the stack element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._peek.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Convert to list for printing\"\"\"\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n  private:\n    ListNode *stackTop; // Use head node as stack top\n    int stkSize;        // Stack length\n\n  public:\n    LinkedListStack() {\n        stackTop = nullptr;\n        stkSize = 0;\n    }\n\n    ~LinkedListStack() {\n        // Traverse linked list to delete nodes and free memory\n        freeMemoryLinkedList(stackTop);\n    }\n\n    /* Get the length of the stack */\n    int size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    void push(int num) {\n        ListNode *node = new ListNode(num);\n        node->next = stackTop;\n        stackTop = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    int pop() {\n        int num = top();\n        ListNode *tmp = stackTop;\n        stackTop = stackTop->next;\n        // Free memory\n        delete tmp;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"Stack is empty\");\n        return stackTop->val;\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    private ListNode stackPeek; // Use head node as stack top\n    private int stkSize = 0; // Stack length\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    public int size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    public void push(int num) {\n        ListNode node = new ListNode(num);\n        node.next = stackPeek;\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    public int pop() {\n        int num = peek();\n        stackPeek = stackPeek.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stackPeek.val;\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    ListNode? stackPeek;  // Use head node as stack top\n    int stkSize = 0;   // Stack length\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    public int Size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Push */\n    public void Push(int num) {\n        ListNode node = new(num) {\n            next = stackPeek\n        };\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    public int Pop() {\n        int num = Peek();\n        stackPeek = stackPeek!.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stackPeek!.val;\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\ntype linkedListStack struct {\n    // Use built-in package list to implement stack\n    data *list.List\n}\n\n/* Access top of the stack element */\nfunc newLinkedListStack() *linkedListStack {\n    return &linkedListStack{\n        data: list.New(),\n    }\n}\n\n/* Push */\nfunc (s *linkedListStack) push(value int) {\n    s.data.PushBack(value)\n}\n\n/* Pop */\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/* Return list for printing */\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/* Get the length of the stack */\nfunc (s *linkedListStack) size() int {\n    return s.data.Len()\n}\n\n/* Check if the stack is empty */\nfunc (s *linkedListStack) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListStack) toList() *list.List {\n    return s.data\n}\n
    linkedlist_stack.swift
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    private var _peek: ListNode? // Use head node as stack top\n    private var _size: Int // Stack length\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the stack */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the stack is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Push */\n    func push(num: Int) {\n        let node = ListNode(x: num)\n        node.next = _peek\n        _peek = node\n        _size += 1\n    }\n\n    /* Pop */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        _peek = _peek?.next\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return _peek!.val\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    #stackPeek; // Use head node as stack top\n    #stkSize = 0; // Stack length\n\n    constructor() {\n        this.#stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    get size() {\n        return this.#stkSize;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Push */\n    push(num) {\n        const node = new ListNode(num);\n        node.next = this.#stackPeek;\n        this.#stackPeek = node;\n        this.#stkSize++;\n    }\n\n    /* Pop */\n    pop() {\n        const num = this.peek();\n        this.#stackPeek = this.#stackPeek.next;\n        this.#stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (!this.#stackPeek) throw new Error('Stack is empty');\n        return this.#stackPeek.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Stack based on linked list implementation */\nclass LinkedListStack {\n    private stackPeek: ListNode | null; // Use head node as stack top\n    private stkSize: number = 0; // Stack length\n\n    constructor() {\n        this.stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    get size(): number {\n        return this.stkSize;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Push */\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    /* Pop */\n    pop(): number {\n        const num = this.peek();\n        if (!this.stackPeek) throw new Error('Stack is empty');\n        this.stackPeek = this.stackPeek.next;\n        this.stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (!this.stackPeek) throw new Error('Stack is empty');\n        return this.stackPeek.val;\n    }\n\n    /* Convert linked list to Array and return */\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
    /* Stack implemented based on linked list class */\nclass LinkedListStack {\n  ListNode? _stackPeek; // Use head node as stack top\n  int _stkSize = 0; // Stack length\n\n  LinkedListStack() {\n    _stackPeek = null;\n  }\n\n  /* Get the length of the stack */\n  int size() {\n    return _stkSize;\n  }\n\n  /* Check if the stack is empty */\n  bool isEmpty() {\n    return _stkSize == 0;\n  }\n\n  /* Push */\n  void push(int _num) {\n    final ListNode node = ListNode(_num);\n    node.next = _stackPeek;\n    _stackPeek = node;\n    _stkSize++;\n  }\n\n  /* Pop */\n  int pop() {\n    final int _num = peek();\n    _stackPeek = _stackPeek!.next;\n    _stkSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (_stackPeek == null) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stackPeek!.val;\n  }\n\n  /* Convert linked list to List and return */\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
    /* Stack based on linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListStack<T> {\n    stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // Use head node as stack top\n    stk_size: usize,                              // Stack length\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    /* Get the length of the stack */\n    pub fn size(&self) -> usize {\n        return self.stk_size;\n    }\n\n    /* Check if the stack is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.size() == 0;\n    }\n\n    /* Push */\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    /* Pop */\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    /* Return list for printing */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.stack_peek.as_ref()\n    }\n\n    /* Convert List to Array and return */\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
    /* Stack based on linked list implementation */\ntypedef struct {\n    ListNode *top; // Use head node as stack top\n    int size;      // Stack length\n} LinkedListStack;\n\n/* Constructor */\nLinkedListStack *newLinkedListStack() {\n    LinkedListStack *s = malloc(sizeof(LinkedListStack));\n    s->top = NULL;\n    s->size = 0;\n    return s;\n}\n\n/* Destructor */\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/* Get the length of the stack */\nint size(LinkedListStack *s) {\n    return s->size;\n}\n\n/* Check if the stack is empty */\nbool isEmpty(LinkedListStack *s) {\n    return size(s) == 0;\n}\n\n/* Push */\nvoid push(LinkedListStack *s, int num) {\n    ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n    node->next = s->top; // Update new node's pointer field\n    node->val = num;     // Update new node's data field\n    s->top = node;       // Update stack top\n    s->size++;           // Update stack size\n}\n\n/* Return list for printing */\nint peek(LinkedListStack *s) {\n    if (s->size == 0) {\n        printf(\"Stack is empty\\n\");\n        return INT_MAX;\n    }\n    return s->top->val;\n}\n\n/* Pop */\nint pop(LinkedListStack *s) {\n    int val = peek(s);\n    ListNode *tmp = s->top;\n    s->top = s->top->next;\n    // Free memory\n    free(tmp);\n    s->size--;\n    return val;\n}\n
    linkedlist_stack.kt
    /* Stack based on linked list implementation */\nclass LinkedListStack(\n    private var stackPeek: ListNode? = null, // Use head node as stack top\n    private var stkSize: Int = 0 // Stack length\n) {\n\n    /* Get the length of the stack */\n    fun size(): Int {\n        return stkSize\n    }\n\n    /* Check if the stack is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Push */\n    fun push(num: Int) {\n        val node = ListNode(num)\n        node.next = stackPeek\n        stackPeek = node\n        stkSize++\n    }\n\n    /* Pop */\n    fun pop(): Int? {\n        val num = peek()\n        stackPeek = stackPeek?.next\n        stkSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int? {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stackPeek?._val\n    }\n\n    /* Convert List to Array and return */\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
    ### Stack based on linked list ###\nclass LinkedListStack\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @size = 0\n  end\n\n  ### Check if stack is empty ###\n  def is_empty?\n    @peek.nil?\n  end\n\n  ### Push ###\n  def push(val)\n    node = ListNode.new(val)\n    node.next = @peek\n    @peek = node\n    @size += 1\n  end\n\n  ### Pop ###\n  def pop\n    num = peek\n    @peek = @peek.next\n    @size -= 1\n    num\n  end\n\n  ### Access top element ###\n  def peek\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @peek.val\n  end\n\n  ### Convert linked list to Array and return ###\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":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

    When implementing a stack using an array, we can treat the end of the array as the top of the stack. As shown in Figure 5-3, push and pop operations correspond to adding and removing elements at the end of the array, both with a time complexity of \\(O(1)\\).

    <1><2><3>

    Figure 5-3   Push and pop operations in array implementation of stack

    Since elements pushed onto the stack may increase continuously, we can use a dynamic array, which eliminates the need to handle array expansion ourselves. Here is the sample code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py
    class ArrayStack:\n    \"\"\"Stack based on array implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._stack: list[int] = []\n\n    def size(self) -> int:\n        \"\"\"Get the length of the stack\"\"\"\n        return len(self._stack)\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the stack is empty\"\"\"\n        return self.size() == 0\n\n    def push(self, item: int):\n        \"\"\"Push\"\"\"\n        self._stack.append(item)\n\n    def pop(self) -> int:\n        \"\"\"Pop\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._stack.pop()\n\n    def peek(self) -> int:\n        \"\"\"Access top of the stack element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._stack[-1]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Return list for printing\"\"\"\n        return self._stack\n
    array_stack.cpp
    /* Stack based on array implementation */\nclass ArrayStack {\n  private:\n    vector<int> stack;\n\n  public:\n    /* Get the length of the stack */\n    int size() {\n        return stack.size();\n    }\n\n    /* Check if the stack is empty */\n    bool isEmpty() {\n        return stack.size() == 0;\n    }\n\n    /* Push */\n    void push(int num) {\n        stack.push_back(num);\n    }\n\n    /* Pop */\n    int pop() {\n        int num = top();\n        stack.pop_back();\n        return num;\n    }\n\n    /* Return list for printing */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"Stack is empty\");\n        return stack.back();\n    }\n\n    /* Return Vector */\n    vector<int> toVector() {\n        return stack;\n    }\n};\n
    array_stack.java
    /* Stack based on array implementation */\nclass ArrayStack {\n    private ArrayList<Integer> stack;\n\n    public ArrayStack() {\n        // Initialize list (dynamic array)\n        stack = new ArrayList<>();\n    }\n\n    /* Get the length of the stack */\n    public int size() {\n        return stack.size();\n    }\n\n    /* Check if the stack is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    public void push(int num) {\n        stack.add(num);\n    }\n\n    /* Pop */\n    public int pop() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.remove(size() - 1);\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.get(size() - 1);\n    }\n\n    /* Convert List to Array and return */\n    public Object[] toArray() {\n        return stack.toArray();\n    }\n}\n
    array_stack.cs
    /* Stack based on array implementation */\nclass ArrayStack {\n    List<int> stack;\n    public ArrayStack() {\n        // Initialize list (dynamic array)\n        stack = [];\n    }\n\n    /* Get the length of the stack */\n    public int Size() {\n        return stack.Count;\n    }\n\n    /* Check if the stack is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Push */\n    public void Push(int num) {\n        stack.Add(num);\n    }\n\n    /* Pop */\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    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stack[Size() - 1];\n    }\n\n    /* Convert List to Array and return */\n    public int[] ToArray() {\n        return [.. stack];\n    }\n}\n
    array_stack.go
    /* Stack based on array implementation */\ntype arrayStack struct {\n    data []int // Data\n}\n\n/* Access top of the stack element */\nfunc newArrayStack() *arrayStack {\n    return &arrayStack{\n        // Set stack length to 0, capacity to 16\n        data: make([]int, 0, 16),\n    }\n}\n\n/* Stack length */\nfunc (s *arrayStack) size() int {\n    return len(s.data)\n}\n\n/* Is stack empty */\nfunc (s *arrayStack) isEmpty() bool {\n    return s.size() == 0\n}\n\n/* Push */\nfunc (s *arrayStack) push(v int) {\n    // Slice will automatically expand\n    s.data = append(s.data, v)\n}\n\n/* Pop */\nfunc (s *arrayStack) pop() any {\n    val := s.peek()\n    s.data = s.data[:len(s.data)-1]\n    return val\n}\n\n/* Get stack top element */\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/* Get Slice for printing */\nfunc (s *arrayStack) toSlice() []int {\n    return s.data\n}\n
    array_stack.swift
    /* Stack based on array implementation */\nclass ArrayStack {\n    private var stack: [Int]\n\n    init() {\n        // Initialize list (dynamic array)\n        stack = []\n    }\n\n    /* Get the length of the stack */\n    func size() -> Int {\n        stack.count\n    }\n\n    /* Check if the stack is empty */\n    func isEmpty() -> Bool {\n        stack.isEmpty\n    }\n\n    /* Push */\n    func push(num: Int) {\n        stack.append(num)\n    }\n\n    /* Pop */\n    @discardableResult\n    func pop() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return stack.removeLast()\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return stack.last!\n    }\n\n    /* Convert List to Array and return */\n    func toArray() -> [Int] {\n        stack\n    }\n}\n
    array_stack.js
    /* Stack based on array implementation */\nclass ArrayStack {\n    #stack;\n    constructor() {\n        this.#stack = [];\n    }\n\n    /* Get the length of the stack */\n    get size() {\n        return this.#stack.length;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty() {\n        return this.#stack.length === 0;\n    }\n\n    /* Push */\n    push(num) {\n        this.#stack.push(num);\n    }\n\n    /* Pop */\n    pop() {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.#stack.pop();\n    }\n\n    /* Return list for printing */\n    top() {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.#stack[this.#stack.length - 1];\n    }\n\n    /* Return Array */\n    toArray() {\n        return this.#stack;\n    }\n}\n
    array_stack.ts
    /* Stack based on array implementation */\nclass ArrayStack {\n    private stack: number[];\n    constructor() {\n        this.stack = [];\n    }\n\n    /* Get the length of the stack */\n    get size(): number {\n        return this.stack.length;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty(): boolean {\n        return this.stack.length === 0;\n    }\n\n    /* Push */\n    push(num: number): void {\n        this.stack.push(num);\n    }\n\n    /* Pop */\n    pop(): number | undefined {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.stack.pop();\n    }\n\n    /* Return list for printing */\n    top(): number | undefined {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.stack[this.stack.length - 1];\n    }\n\n    /* Return Array */\n    toArray() {\n        return this.stack;\n    }\n}\n
    array_stack.dart
    /* Stack based on array implementation */\nclass ArrayStack {\n  late List<int> _stack;\n  ArrayStack() {\n    _stack = [];\n  }\n\n  /* Get the length of the stack */\n  int size() {\n    return _stack.length;\n  }\n\n  /* Check if the stack is empty */\n  bool isEmpty() {\n    return _stack.isEmpty;\n  }\n\n  /* Push */\n  void push(int _num) {\n    _stack.add(_num);\n  }\n\n  /* Pop */\n  int pop() {\n    if (isEmpty()) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stack.removeLast();\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stack.last;\n  }\n\n  /* Convert stack to Array and return */\n  List<int> toArray() => _stack;\n}\n
    array_stack.rs
    /* Stack based on array implementation */\nstruct ArrayStack<T> {\n    stack: Vec<T>,\n}\n\nimpl<T> ArrayStack<T> {\n    /* Access top of the stack element */\n    fn new() -> ArrayStack<T> {\n        ArrayStack::<T> {\n            stack: Vec::<T>::new(),\n        }\n    }\n\n    /* Get the length of the stack */\n    fn size(&self) -> usize {\n        self.stack.len()\n    }\n\n    /* Check if the stack is empty */\n    fn is_empty(&self) -> bool {\n        self.size() == 0\n    }\n\n    /* Push */\n    fn push(&mut self, num: T) {\n        self.stack.push(num);\n    }\n\n    /* Pop */\n    fn pop(&mut self) -> Option<T> {\n        self.stack.pop()\n    }\n\n    /* Return list for printing */\n    fn peek(&self) -> Option<&T> {\n        if self.is_empty() {\n            panic!(\"Stack is empty\")\n        };\n        self.stack.last()\n    }\n\n    /* Return &Vec */\n    fn to_array(&self) -> &Vec<T> {\n        &self.stack\n    }\n}\n
    array_stack.c
    /* Stack based on array implementation */\ntypedef struct {\n    int *data;\n    int size;\n} ArrayStack;\n\n/* Constructor */\nArrayStack *newArrayStack() {\n    ArrayStack *stack = malloc(sizeof(ArrayStack));\n    // Initialize with large capacity to avoid expansion\n    stack->data = malloc(sizeof(int) * MAX_SIZE);\n    stack->size = 0;\n    return stack;\n}\n\n/* Destructor */\nvoid delArrayStack(ArrayStack *stack) {\n    free(stack->data);\n    free(stack);\n}\n\n/* Get the length of the stack */\nint size(ArrayStack *stack) {\n    return stack->size;\n}\n\n/* Check if the stack is empty */\nbool isEmpty(ArrayStack *stack) {\n    return stack->size == 0;\n}\n\n/* Push */\nvoid push(ArrayStack *stack, int num) {\n    if (stack->size == MAX_SIZE) {\n        printf(\"Stack is full\\n\");\n        return;\n    }\n    stack->data[stack->size] = num;\n    stack->size++;\n}\n\n/* Return list for printing */\nint peek(ArrayStack *stack) {\n    if (stack->size == 0) {\n        printf(\"Stack is empty\\n\");\n        return INT_MAX;\n    }\n    return stack->data[stack->size - 1];\n}\n\n/* Pop */\nint pop(ArrayStack *stack) {\n    int val = peek(stack);\n    stack->size--;\n    return val;\n}\n
    array_stack.kt
    /* Stack based on array implementation */\nclass ArrayStack {\n    // Initialize list (dynamic array)\n    private val stack = mutableListOf<Int>()\n\n    /* Get the length of the stack */\n    fun size(): Int {\n        return stack.size\n    }\n\n    /* Check if the stack is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Push */\n    fun push(num: Int) {\n        stack.add(num)\n    }\n\n    /* Pop */\n    fun pop(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack.removeAt(size() - 1)\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack[size() - 1]\n    }\n\n    /* Convert List to Array and return */\n    fun toArray(): Array<Any> {\n        return stack.toTypedArray()\n    }\n}\n
    array_stack.rb
    ### Stack based on array ###\nclass ArrayStack\n  ### Constructor ###\n  def initialize\n    @stack = []\n  end\n\n  ### Get stack length ###\n  def size\n    @stack.length\n  end\n\n  ### Check if stack is empty ###\n  def is_empty?\n    @stack.empty?\n  end\n\n  ### Push ###\n  def push(item)\n    @stack << item\n  end\n\n  ### Pop ###\n  def pop\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @stack.pop\n  end\n\n  ### Access top element ###\n  def peek\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @stack.last\n  end\n\n  ### Return list for printing ###\n  def to_array\n    @stack\n  end\nend\n
    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513-comparison-of-the-two-implementations","level":2,"title":"5.1.3   Comparison of the Two Implementations","text":"

    Supported Operations

    Both implementations support all operations defined by the stack. The array implementation additionally supports random access, but this goes beyond the stack definition and is generally not used.

    Time Efficiency

    In the array-based implementation, both push and pop operations occur in pre-allocated contiguous memory, which has good cache locality and is therefore more efficient. However, if pushing exceeds the array capacity, it triggers an expansion mechanism, causing the time complexity of that particular push operation to become \\(O(n)\\).

    In the linked list-based implementation, list expansion is very flexible, and there is no issue of reduced efficiency due to array expansion. However, the push operation requires initializing a node object and modifying pointers, so it is relatively less efficient. Nevertheless, if the pushed elements are already node objects, the initialization step can be omitted, thereby improving efficiency.

    In summary, when the elements pushed and popped are basic data types such as int or double, we can draw the following conclusions:

    • The array-based stack implementation has reduced efficiency when expansion is triggered, but since expansion is an infrequent operation, the average efficiency is higher.
    • The linked list-based stack implementation can provide more stable efficiency performance.

    Space Efficiency

    When initializing a list, the system allocates an \"initial capacity\" that may exceed the actual need. Additionally, the expansion mechanism typically expands at a specific ratio (e.g., 2x), and the capacity after expansion may also exceed actual needs. Therefore, the array-based stack implementation may cause some space wastage.

    However, since linked list nodes need to store additional pointers, the space occupied by linked list nodes is relatively large.

    In summary, we cannot simply determine which implementation is more memory-efficient and need to analyze the specific situation.

    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514-typical-applications-of-stack","level":2,"title":"5.1.4   Typical Applications of Stack","text":"
    • Back and forward in browsers, undo and redo in software. Every time we open a new webpage, the browser pushes the previous page onto the stack, allowing us to return to the previous page via the back operation. The back operation is essentially performing a pop. To support both back and forward, two stacks are needed to work together.
    • Program memory management. Each time a function is called, the system adds a stack frame to the top of the stack to record the function's context information. During recursion, the downward recursive phase continuously performs push operations, while the upward backtracking phase continuously performs pop operations.
    ","path":["Chapter 5. Stacks and Queues","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   Summary","text":"","path":["Chapter 5. Stacks and Queues","5.4   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • A stack is a data structure that follows the LIFO principle and can be implemented using arrays or linked lists.
    • In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to \\(O(n)\\). In contrast, the linked-list implementation of a stack offers more stable performance.
    • In terms of space efficiency, the array implementation of a stack may lead to some degree of space wastage. However, it should be noted that the memory space occupied by linked list nodes is larger than that of array elements.
    • A queue is a data structure that follows the FIFO principle and can also be implemented using arrays or linked lists. The conclusions regarding time efficiency and space efficiency comparisons for queues are similar to those for stacks mentioned above.
    • A deque is a queue with greater flexibility that allows adding and removing elements at both ends.
    ","path":["Chapter 5. Stacks and Queues","5.4   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: Is the browser's forward and backward functionality implemented with a doubly linked list?

    The browser's forward and backward behavior is essentially an application of a \"stack.\" When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. A deque can conveniently support some additional operations, as mentioned in the \"Deque\" section.

    Q: After popping from the stack, do we need to free the memory of the popped node?

    If the popped node will still be needed later, then memory does not need to be freed. If it won't be used afterward, languages like Java and Python have automatic garbage collection, so manual memory deallocation is not required; in C and C++, manual memory deallocation is necessary.

    Q: A deque seems like two stacks joined together. What is its purpose?

    A deque is like a combination of a stack and a queue, or two stacks joined together. It combines the logic of both, so it can support all applications of stacks and queues while offering greater flexibility.

    Q: How are undo and redo specifically implemented?

    Use two stacks: stack A for undo and stack B for redo.

    1. Whenever the user performs an operation, push this operation onto stack A and clear stack B.
    2. When the user performs \"undo,\" pop the most recent operation from stack A and push it onto stack B.
    3. When the user performs \"redo,\" pop the most recent operation from stack B and push it onto stack A.
    ","path":["Chapter 5. Stacks and Queues","5.4   Summary"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"Chapter 7.   Tree","text":"

    Abstract

    Towering trees are full of vitality, with deep roots, lush foliage, and sprawling branches.

    They offer a vivid illustration of divide-and-conquer in data structures.

    ","path":["Chapter 7. Tree","Chapter 7.   Tree"],"tags":[]},{"location":"chapter_tree/#chapter-contents","level":2,"title":"Chapter contents","text":"
    • 7.1   Binary Tree
    • 7.2   Binary Tree Traversal
    • 7.3   Array Representation of Binary Trees
    • 7.4   Binary Search Tree
    • 7.5   AVL Tree *
    • 7.6   Summary
    ","path":["Chapter 7. Tree","Chapter 7.   Tree"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   Array Representation of Binary Trees","text":"

    In the linked-list representation, the storage unit of a binary tree is a node TreeNode, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees in this representation.

    So, can we use an array to represent a binary tree? The answer is yes.

    ","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731-representing-perfect-binary-trees","level":2,"title":"7.3.1   Representing Perfect Binary Trees","text":"

    Let's analyze a simple case first. Given a perfect binary tree, we store all nodes in an array according to the order of level-order traversal, where each node corresponds to a unique array index.

    Based on the characteristics of level-order traversal, we can derive a \"mapping formula\" between parent node index and child node indices: If a node's index is \\(i\\), then its left child index is \\(2i + 1\\) and its right child index is \\(2i + 2\\). Figure 7-12 shows the mapping relationships between various node indices.

    Figure 7-12   Array representation of a perfect binary tree

    The mapping formula plays a role similar to the node references (pointers) in linked lists. Given any node in the array, we can access its left (right) child node using the mapping formula.

    ","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732-representing-any-binary-tree","level":2,"title":"7.3.2   Representing Any Binary Tree","text":"

    Perfect binary trees are a special case; in the middle levels of a binary tree, there are typically many None values. Since the level-order traversal sequence does not include these None values, we cannot infer the number and distribution of None values based on this sequence alone. This means multiple binary tree structures can correspond to the same level-order traversal sequence.

    As shown in Figure 7-13, given a non-perfect binary tree, the above method of array representation fails.

    Figure 7-13   Level-order traversal sequence corresponds to multiple binary tree possibilities

    To solve this problem, we can explicitly write out all None values in the level-order traversal sequence. As shown in Figure 7-14, once we do this, the level-order traversal sequence can uniquely represent a binary tree. Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # Array representation of a binary tree\n# Using None to represent empty slots\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n
    /* Array representation of a binary tree */\n// Using the maximum integer value INT_MAX to mark empty slots\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
    /* Array representation of a binary tree */\n// Using the Integer wrapper class allows for using null to mark empty slots\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
    /* Array representation of a binary tree */\n// Using nullable int (int?) allows for using null to mark empty slots\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
    /* Array representation of a binary tree */\n// Using an any type slice, allowing for nil to mark empty slots\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n
    /* Array representation of a binary tree */\n// Using optional Int (Int?) allows for using nil to mark empty slots\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n
    /* Array representation of a binary tree */\n// Using null to represent empty slots\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
    /* Array representation of a binary tree */\n// Using null to represent empty slots\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
    /* Array representation of a binary tree */\n// Using nullable int (int?) allows for using null to mark empty slots\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
    /* Array representation of a binary tree */\n// Using None to mark empty slots\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
    /* Array representation of a binary tree */\n// Using the maximum int value to mark empty slots, therefore, node values must not be 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
    /* Array representation of a binary tree */\n// Using null to represent empty slots\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n
    ### Array representation of a binary tree ###\n# Using nil to represent empty slots\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n

    Figure 7-14   Array representation of an arbitrary binary tree

    It's worth noting that complete binary trees are very well-suited for array representation. Recalling the definition of a complete binary tree, None only appears at the bottom level and towards the right, meaning all None values must appear at the end of the level-order traversal sequence.

    This means that when using an array to represent a complete binary tree, it's possible to omit storing all None values, which is very convenient. Figure 7-15 gives an example.

    Figure 7-15   Array representation of a complete binary tree

    The following code implements a binary tree using an array representation, including the following operations:

    • Given a node, obtain its value, left (right) child node, and parent node.
    • Obtain the preorder, inorder, postorder, and level-order traversal sequences.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py
    class ArrayBinaryTree:\n    \"\"\"Binary tree class represented by array\"\"\"\n\n    def __init__(self, arr: list[int | None]):\n        \"\"\"Constructor\"\"\"\n        self._tree = list(arr)\n\n    def size(self):\n        \"\"\"List capacity\"\"\"\n        return len(self._tree)\n\n    def val(self, i: int) -> int | None:\n        \"\"\"Get value of node at index i\"\"\"\n        # If index is out of bounds, return None, representing empty position\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        \"\"\"Get index of left child node of node at index i\"\"\"\n        return 2 * i + 1\n\n    def right(self, i: int) -> int | None:\n        \"\"\"Get index of right child node of node at index i\"\"\"\n        return 2 * i + 2\n\n    def parent(self, i: int) -> int | None:\n        \"\"\"Get index of parent node of node at index i\"\"\"\n        return (i - 1) // 2\n\n    def level_order(self) -> list[int]:\n        \"\"\"Level-order traversal\"\"\"\n        self.res = []\n        # Traverse array directly\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        \"\"\"Depth-first traversal\"\"\"\n        if self.val(i) is None:\n            return\n        # Preorder traversal\n        if order == \"pre\":\n            self.res.append(self.val(i))\n        self.dfs(self.left(i), order)\n        # Inorder traversal\n        if order == \"in\":\n            self.res.append(self.val(i))\n        self.dfs(self.right(i), order)\n        # Postorder traversal\n        if order == \"post\":\n            self.res.append(self.val(i))\n\n    def pre_order(self) -> list[int]:\n        \"\"\"Preorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"pre\")\n        return self.res\n\n    def in_order(self) -> list[int]:\n        \"\"\"Inorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"in\")\n        return self.res\n\n    def post_order(self) -> list[int]:\n        \"\"\"Postorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"post\")\n        return self.res\n
    array_binary_tree.cpp
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n  public:\n    /* Constructor */\n    ArrayBinaryTree(vector<int> arr) {\n        tree = arr;\n    }\n\n    /* List capacity */\n    int size() {\n        return tree.size();\n    }\n\n    /* Get value of node at index i */\n    int val(int i) {\n        // Return INT_MAX if index out of bounds, representing empty position\n        if (i < 0 || i >= size())\n            return INT_MAX;\n        return tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    int left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    int right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    int parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    vector<int> levelOrder() {\n        vector<int> res;\n        // Traverse array directly\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    /* Preorder traversal */\n    vector<int> preOrder() {\n        vector<int> res;\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    vector<int> inOrder() {\n        vector<int> res;\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\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    /* Depth-first traversal */\n    void dfs(int i, string order, vector<int> &res) {\n        // If empty position, return\n        if (val(i) == INT_MAX)\n            return;\n        // Preorder traversal\n        if (order == \"pre\")\n            res.push_back(val(i));\n        dfs(left(i), order, res);\n        // Inorder traversal\n        if (order == \"in\")\n            res.push_back(val(i));\n        dfs(right(i), order, res);\n        // Postorder traversal\n        if (order == \"post\")\n            res.push_back(val(i));\n    }\n};\n
    array_binary_tree.java
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    private List<Integer> tree;\n\n    /* Constructor */\n    public ArrayBinaryTree(List<Integer> arr) {\n        tree = new ArrayList<>(arr);\n    }\n\n    /* List capacity */\n    public int size() {\n        return tree.size();\n    }\n\n    /* Get value of node at index i */\n    public Integer val(int i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= size())\n            return null;\n        return tree.get(i);\n    }\n\n    /* Get index of left child node of node at index i */\n    public Integer left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    public Integer right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    public Integer parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    public List<Integer> levelOrder() {\n        List<Integer> res = new ArrayList<>();\n        // Traverse array directly\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    /* Depth-first traversal */\n    private void dfs(Integer i, String order, List<Integer> res) {\n        // If empty position, return\n        if (val(i) == null)\n            return;\n        // Preorder traversal\n        if (\"pre\".equals(order))\n            res.add(val(i));\n        dfs(left(i), order, res);\n        // Inorder traversal\n        if (\"in\".equals(order))\n            res.add(val(i));\n        dfs(right(i), order, res);\n        // Postorder traversal\n        if (\"post\".equals(order))\n            res.add(val(i));\n    }\n\n    /* Preorder traversal */\n    public List<Integer> preOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    public List<Integer> inOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\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
    /* Binary tree class represented by array */\nclass ArrayBinaryTree(List<int?> arr) {\n    List<int?> tree = new(arr);\n\n    /* List capacity */\n    public int Size() {\n        return tree.Count;\n    }\n\n    /* Get value of node at index i */\n    public int? Val(int i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= Size())\n            return null;\n        return tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    public int Left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    public int Right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    public int Parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    public List<int> LevelOrder() {\n        List<int> res = [];\n        // Traverse array directly\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    /* Depth-first traversal */\n    void DFS(int i, string order, List<int> res) {\n        // If empty position, return\n        if (!Val(i).HasValue)\n            return;\n        // Preorder traversal\n        if (order == \"pre\")\n            res.Add(Val(i)!.Value);\n        DFS(Left(i), order, res);\n        // Inorder traversal\n        if (order == \"in\")\n            res.Add(Val(i)!.Value);\n        DFS(Right(i), order, res);\n        // Postorder traversal\n        if (order == \"post\")\n            res.Add(Val(i)!.Value);\n    }\n\n    /* Preorder traversal */\n    public List<int> PreOrder() {\n        List<int> res = [];\n        DFS(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    public List<int> InOrder() {\n        List<int> res = [];\n        DFS(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    public List<int> PostOrder() {\n        List<int> res = [];\n        DFS(0, \"post\", res);\n        return res;\n    }\n}\n
    array_binary_tree.go
    /* Binary tree class represented by array */\ntype arrayBinaryTree struct {\n    tree []any\n}\n\n/* Constructor */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n    return &arrayBinaryTree{\n        tree: arr,\n    }\n}\n\n/* List capacity */\nfunc (abt *arrayBinaryTree) size() int {\n    return len(abt.tree)\n}\n\n/* Get value of node at index i */\nfunc (abt *arrayBinaryTree) val(i int) any {\n    // If index out of bounds, return null to represent empty position\n    if i < 0 || i >= abt.size() {\n        return nil\n    }\n    return abt.tree[i]\n}\n\n/* Get index of left child node of node at index i */\nfunc (abt *arrayBinaryTree) left(i int) int {\n    return 2*i + 1\n}\n\n/* Get index of right child node of node at index i */\nfunc (abt *arrayBinaryTree) right(i int) int {\n    return 2*i + 2\n}\n\n/* Get index of parent node of node at index i */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n    return (i - 1) / 2\n}\n\n/* Level-order traversal */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n    var res []any\n    // Traverse array directly\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/* Depth-first traversal */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n    // If empty position, return\n    if abt.val(i) == nil {\n        return\n    }\n    // Preorder traversal\n    if order == \"pre\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.left(i), order, res)\n    // Inorder traversal\n    if order == \"in\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.right(i), order, res)\n    // Postorder traversal\n    if order == \"post\" {\n        *res = append(*res, abt.val(i))\n    }\n}\n\n/* Preorder traversal */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n    var res []any\n    abt.dfs(0, \"pre\", &res)\n    return res\n}\n\n/* Inorder traversal */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n    var res []any\n    abt.dfs(0, \"in\", &res)\n    return res\n}\n\n/* Postorder traversal */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n    var res []any\n    abt.dfs(0, \"post\", &res)\n    return res\n}\n
    array_binary_tree.swift
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    private var tree: [Int?]\n\n    /* Constructor */\n    init(arr: [Int?]) {\n        tree = arr\n    }\n\n    /* List capacity */\n    func size() -> Int {\n        tree.count\n    }\n\n    /* Get value of node at index i */\n    func val(i: Int) -> Int? {\n        // If index out of bounds, return null to represent empty position\n        if i < 0 || i >= size() {\n            return nil\n        }\n        return tree[i]\n    }\n\n    /* Get index of left child node of node at index i */\n    func left(i: Int) -> Int {\n        2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    func right(i: Int) -> Int {\n        2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    func parent(i: Int) -> Int {\n        (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    func levelOrder() -> [Int] {\n        var res: [Int] = []\n        // Traverse array directly\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    /* Depth-first traversal */\n    private func dfs(i: Int, order: String, res: inout [Int]) {\n        // If empty position, return\n        guard let val = val(i: i) else {\n            return\n        }\n        // Preorder traversal\n        if order == \"pre\" {\n            res.append(val)\n        }\n        dfs(i: left(i: i), order: order, res: &res)\n        // Inorder traversal\n        if order == \"in\" {\n            res.append(val)\n        }\n        dfs(i: right(i: i), order: order, res: &res)\n        // Postorder traversal\n        if order == \"post\" {\n            res.append(val)\n        }\n    }\n\n    /* Preorder traversal */\n    func preOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"pre\", res: &res)\n        return res\n    }\n\n    /* Inorder traversal */\n    func inOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"in\", res: &res)\n        return res\n    }\n\n    /* Postorder traversal */\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
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    #tree;\n\n    /* Constructor */\n    constructor(arr) {\n        this.#tree = arr;\n    }\n\n    /* List capacity */\n    size() {\n        return this.#tree.length;\n    }\n\n    /* Get value of node at index i */\n    val(i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    left(i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    right(i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    parent(i) {\n        return Math.floor((i - 1) / 2); // Floor division\n    }\n\n    /* Level-order traversal */\n    levelOrder() {\n        let res = [];\n        // Traverse array directly\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    /* Depth-first traversal */\n    #dfs(i, order, res) {\n        // If empty position, return\n        if (this.val(i) === null) return;\n        // Preorder traversal\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Inorder traversal\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Postorder traversal\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Preorder traversal */\n    preOrder() {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    inOrder() {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    postOrder() {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
    array_binary_tree.ts
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    #tree: (number | null)[];\n\n    /* Constructor */\n    constructor(arr: (number | null)[]) {\n        this.#tree = arr;\n    }\n\n    /* List capacity */\n    size(): number {\n        return this.#tree.length;\n    }\n\n    /* Get value of node at index i */\n    val(i: number): number | null {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    left(i: number): number {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    right(i: number): number {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    parent(i: number): number {\n        return Math.floor((i - 1) / 2); // Floor division\n    }\n\n    /* Level-order traversal */\n    levelOrder(): number[] {\n        let res = [];\n        // Traverse array directly\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    /* Depth-first traversal */\n    #dfs(i: number, order: Order, res: (number | null)[]): void {\n        // If empty position, return\n        if (this.val(i) === null) return;\n        // Preorder traversal\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Inorder traversal\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Postorder traversal\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Preorder traversal */\n    preOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    inOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    postOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
    array_binary_tree.dart
    /* Binary tree class represented by array */\nclass ArrayBinaryTree {\n  late List<int?> _tree;\n\n  /* Constructor */\n  ArrayBinaryTree(this._tree);\n\n  /* List capacity */\n  int size() {\n    return _tree.length;\n  }\n\n  /* Get value of node at index i */\n  int? val(int i) {\n    // If index out of bounds, return null to represent empty position\n    if (i < 0 || i >= size()) {\n      return null;\n    }\n    return _tree[i];\n  }\n\n  /* Get index of left child node of node at index i */\n  int? left(int i) {\n    return 2 * i + 1;\n  }\n\n  /* Get index of right child node of node at index i */\n  int? right(int i) {\n    return 2 * i + 2;\n  }\n\n  /* Get index of parent node of node at index i */\n  int? parent(int i) {\n    return (i - 1) ~/ 2;\n  }\n\n  /* Level-order traversal */\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  /* Depth-first traversal */\n  void dfs(int i, String order, List<int?> res) {\n    // If empty position, return\n    if (val(i) == null) {\n      return;\n    }\n    // Preorder traversal\n    if (order == 'pre') {\n      res.add(val(i));\n    }\n    dfs(left(i)!, order, res);\n    // Inorder traversal\n    if (order == 'in') {\n      res.add(val(i));\n    }\n    dfs(right(i)!, order, res);\n    // Postorder traversal\n    if (order == 'post') {\n      res.add(val(i));\n    }\n  }\n\n  /* Preorder traversal */\n  List<int?> preOrder() {\n    List<int?> res = [];\n    dfs(0, 'pre', res);\n    return res;\n  }\n\n  /* Inorder traversal */\n  List<int?> inOrder() {\n    List<int?> res = [];\n    dfs(0, 'in', res);\n    return res;\n  }\n\n  /* Postorder traversal */\n  List<int?> postOrder() {\n    List<int?> res = [];\n    dfs(0, 'post', res);\n    return res;\n  }\n}\n
    array_binary_tree.rs
    /* Binary tree class represented by array */\nstruct ArrayBinaryTree {\n    tree: Vec<Option<i32>>,\n}\n\nimpl ArrayBinaryTree {\n    /* Constructor */\n    fn new(arr: Vec<Option<i32>>) -> Self {\n        Self { tree: arr }\n    }\n\n    /* List capacity */\n    fn size(&self) -> i32 {\n        self.tree.len() as i32\n    }\n\n    /* Get value of node at index i */\n    fn val(&self, i: i32) -> Option<i32> {\n        // If index is out of bounds, return None, representing empty position\n        if i < 0 || i >= self.size() {\n            None\n        } else {\n            self.tree[i as usize]\n        }\n    }\n\n    /* Get index of left child node of node at index i */\n    fn left(&self, i: i32) -> i32 {\n        2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    fn right(&self, i: i32) -> i32 {\n        2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    fn parent(&self, i: i32) -> i32 {\n        (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    fn level_order(&self) -> Vec<i32> {\n        self.tree.iter().filter_map(|&x| x).collect()\n    }\n\n    /* Depth-first traversal */\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        // Preorder traversal\n        if order == \"pre\" {\n            res.push(val);\n        }\n        self.dfs(self.left(i), order, res);\n        // Inorder traversal\n        if order == \"in\" {\n            res.push(val);\n        }\n        self.dfs(self.right(i), order, res);\n        // Postorder traversal\n        if order == \"post\" {\n            res.push(val);\n        }\n    }\n\n    /* Preorder traversal */\n    fn pre_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"pre\", &mut res);\n        res\n    }\n\n    /* Inorder traversal */\n    fn in_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"in\", &mut res);\n        res\n    }\n\n    /* Postorder traversal */\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
    /* Binary tree structure in array representation */\ntypedef struct {\n    int *tree;\n    int size;\n} ArrayBinaryTree;\n\n/* Constructor */\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/* Destructor */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n    free(abt->tree);\n    free(abt);\n}\n\n/* List capacity */\nint size(ArrayBinaryTree *abt) {\n    return abt->size;\n}\n\n/* Get value of node at index i */\nint val(ArrayBinaryTree *abt, int i) {\n    // Return INT_MAX if index out of bounds, representing empty position\n    if (i < 0 || i >= size(abt))\n        return INT_MAX;\n    return abt->tree[i];\n}\n\n/* Level-order traversal */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    // Traverse array directly\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/* Depth-first traversal */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n    // If empty position, return\n    if (val(abt, i) == INT_MAX)\n        return;\n    // Preorder traversal\n    if (strcmp(order, \"pre\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, left(i), order, res, index);\n    // Inorder traversal\n    if (strcmp(order, \"in\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, right(i), order, res, index);\n    // Postorder traversal\n    if (strcmp(order, \"post\") == 0)\n        res[(*index)++] = val(abt, i);\n}\n\n/* Preorder traversal */\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/* Inorder traversal */\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/* Postorder traversal */\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
    /* Binary tree class represented by array */\nclass ArrayBinaryTree(val tree: MutableList<Int?>) {\n    /* List capacity */\n    fun size(): Int {\n        return tree.size\n    }\n\n    /* Get value of node at index i */\n    fun _val(i: Int): Int? {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= size()) return null\n        return tree[i]\n    }\n\n    /* Get index of left child node of node at index i */\n    fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    fun parent(i: Int): Int {\n        return (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    fun levelOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        // Traverse array directly\n        for (i in 0..<size()) {\n            if (_val(i) != null)\n                res.add(_val(i))\n        }\n        return res\n    }\n\n    /* Depth-first traversal */\n    fun dfs(i: Int, order: String, res: MutableList<Int?>) {\n        // If empty position, return\n        if (_val(i) == null)\n            return\n        // Preorder traversal\n        if (\"pre\" == order)\n            res.add(_val(i))\n        dfs(left(i), order, res)\n        // Inorder traversal\n        if (\"in\" == order)\n            res.add(_val(i))\n        dfs(right(i), order, res)\n        // Postorder traversal\n        if (\"post\" == order)\n            res.add(_val(i))\n    }\n\n    /* Preorder traversal */\n    fun preOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"pre\", res)\n        return res\n    }\n\n    /* Inorder traversal */\n    fun inOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"in\", res)\n        return res\n    }\n\n    /* Postorder traversal */\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
    ### Array representation of binary tree class ###\nclass ArrayBinaryTree\n  ### Constructor ###\n  def initialize(arr)\n    @tree = arr.to_a\n  end\n\n  ### List capacity ###\n  def size\n    @tree.length\n  end\n\n  ### Get value of node at index i ###\n  def val(i)\n    # Return nil if index out of bounds, representing empty position\n    return if i < 0 || i >= size\n\n    @tree[i]\n  end\n\n  ### Get left child index of node at index i ###\n  def left(i)\n    2 * i + 1\n  end\n\n  ### Get right child index of node at index i ###\n  def right(i)\n    2 * i + 2\n  end\n\n  ### Get parent node index of node at index i ###\n  def parent(i)\n    (i - 1) / 2\n  end\n\n  ### Level-order traversal ###\n  def level_order\n    @res = []\n\n    # Traverse array directly\n    for i in 0...size\n      @res << val(i) unless val(i).nil?\n    end\n\n    @res\n  end\n\n  ### Depth-first traversal ###\n  def dfs(i, order)\n    return if val(i).nil?\n    # Preorder traversal\n    @res << val(i) if order == :pre\n    dfs(left(i), order)\n    # Inorder traversal\n    @res << val(i) if order == :in\n    dfs(right(i), order)\n    # Postorder traversal\n    @res << val(i) if order == :post\n  end\n\n  ### Pre-order traversal ###\n  def pre_order\n    @res = []\n    dfs(0, :pre)\n    @res\n  end\n\n  ### In-order traversal ###\n  def in_order\n    @res = []\n    dfs(0, :in)\n    @res\n  end\n\n  ### Post-order traversal ###\n  def post_order\n    @res = []\n    dfs(0, :post)\n    @res\n  end\nend\n
    ","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733-advantages-and-limitations","level":2,"title":"7.3.3   Advantages and Limitations","text":"

    The array representation of binary trees has the following advantages:

    • Arrays are stored in contiguous memory space, which is cache-friendly, allowing faster access and traversal.
    • It does not require storing pointers, which saves space.
    • It allows random access to nodes.

    However, the array representation also has some limitations:

    • Array storage requires contiguous memory space, so it is not suitable for storing trees with a large amount of data.
    • Adding or removing nodes requires array insertion and deletion operations, which have lower efficiency.
    • When there are many None values in the binary tree, the proportion of node data contained in the array is low, leading to lower space utilization.
    ","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   AVL Tree *","text":"

    In the \"Binary Search Tree\" section, we mentioned that after multiple insertion and removal operations, a binary search tree may degenerate into a linked list. In this case, the time complexity of all operations degrades from \\(O(\\log n)\\) to \\(O(n)\\).

    As shown in Figure 7-24, after two node removal operations, this binary search tree will degrade into a linked list.

    Figure 7-24   Degradation of an AVL tree after removing nodes

    For example, in the perfect binary tree shown in Figure 7-25, after inserting two nodes, the tree will lean heavily to the left, and the time complexity of search operations will also degrade.

    Figure 7-25   Degradation of an AVL tree after inserting nodes

    In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL tree in their paper \"An algorithm for the organization of information\". The paper describes a series of operations that prevent an AVL tree from degenerating as nodes are inserted and removed, thereby keeping the time complexity of various operations at \\(O(\\log n)\\). In other words, in scenarios that require frequent insertion, deletion, lookup, and update operations, AVL trees can maintain consistently efficient performance and therefore have strong practical value.

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-common-terminology-in-avl-trees","level":2,"title":"7.5.1   Common Terminology in AVL Trees","text":"

    An AVL tree is both a binary search tree and a balanced binary tree, simultaneously satisfying all the properties of these two types of binary trees, hence it is a balanced binary search tree.

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-node-height","level":3,"title":"1.   Node Height","text":"

    Since the operations related to AVL trees require obtaining node heights, we need to add a height variable to the node class:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class TreeNode:\n    \"\"\"AVL tree node\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                 # Node value\n        self.height: int = 0                # Node height\n        self.left: TreeNode | None = None   # Left child reference\n        self.right: TreeNode | None = None  # Right child reference\n
    /* AVL tree node */\nstruct TreeNode {\n    int val{};          // Node value\n    int height = 0;     // Node height\n    TreeNode *left{};   // Left child\n    TreeNode *right{};  // Right child\n    TreeNode() = default;\n    explicit TreeNode(int x) : val(x){}\n};\n
    /* AVL tree node */\nclass TreeNode {\n    public int val;        // Node value\n    public int height;     // Node height\n    public TreeNode left;  // Left child\n    public TreeNode right; // Right child\n    public TreeNode(int x) { val = x; }\n}\n
    /* AVL tree node */\nclass TreeNode(int? x) {\n    public int? val = x;    // Node value\n    public int height;      // Node height\n    public TreeNode? left;  // Left child reference\n    public TreeNode? right; // Right child reference\n}\n
    /* AVL tree node */\ntype TreeNode struct {\n    Val    int       // Node value\n    Height int       // Node height\n    Left   *TreeNode // Left child reference\n    Right  *TreeNode // Right child reference\n}\n
    /* AVL tree node */\nclass TreeNode {\n    var val: Int // Node value\n    var height: Int // Node height\n    var left: TreeNode? // Left child\n    var right: TreeNode? // Right child\n\n    init(x: Int) {\n        val = x\n        height = 0\n    }\n}\n
    /* AVL tree node */\nclass TreeNode {\n    val; // Node value\n    height; // Node height\n    left; // Left child pointer\n    right; // Right child pointer\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 tree node */\nclass TreeNode {\n    val: number;            // Node value\n    height: number;         // Node height\n    left: TreeNode | null;  // Left child pointer\n    right: TreeNode | null; // Right child pointer\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 tree node */\nclass TreeNode {\n  int val;         // Node value\n  int height;      // Node height\n  TreeNode? left;  // Left child\n  TreeNode? right; // Right child\n  TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* AVL tree node */\nstruct TreeNode {\n    val: i32,                               // Node value\n    height: i32,                            // Node height\n    left: Option<Rc<RefCell<TreeNode>>>,    // Left child\n    right: Option<Rc<RefCell<TreeNode>>>,   // Right child\n}\n\nimpl TreeNode {\n    /* Constructor */\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 tree node */\ntypedef struct TreeNode {\n    int val;\n    int height;\n    struct TreeNode *left;\n    struct TreeNode *right;\n} TreeNode;\n\n/* Constructor */\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 tree node */\nclass TreeNode(val _val: Int) {  // Node value\n    val height: Int = 0          // Node height\n    val left: TreeNode? = null   // Left child\n    val right: TreeNode? = null  // Right child\n}\n
    ### AVL tree node class ###\nclass TreeNode\n  attr_accessor :val    # Node value\n  attr_accessor :height # Node height\n  attr_accessor :left   # Left child reference\n  attr_accessor :right  # Right child reference\n\n  def initialize(val)\n    @val = val\n    @height = 0\n  end\nend\n

    The \"node height\" refers to the distance from that node to its farthest leaf node, i.e., the number of edges on the path. It is important to note that the height of a leaf node is \\(0\\), and the height of a null node is \\(-1\\). We will create two utility functions for getting and updating the height of a node:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def height(self, node: TreeNode | None) -> int:\n    \"\"\"Get node height\"\"\"\n    # Empty node height is -1, leaf node height is 0\n    if node is not None:\n        return node.height\n    return -1\n\ndef update_height(self, node: TreeNode | None):\n    \"\"\"Update node height\"\"\"\n    # Node height equals the height of the tallest subtree + 1\n    node.height = max([self.height(node.left), self.height(node.right)]) + 1\n
    avl_tree.cpp
    /* Get node height */\nint height(TreeNode *node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == nullptr ? -1 : node->height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode *node) {\n    // Node height equals the height of the tallest subtree + 1\n    node->height = max(height(node->left), height(node->right)) + 1;\n}\n
    avl_tree.java
    /* Get node height */\nint height(TreeNode node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n
    avl_tree.cs
    /* Get node height */\nint Height(TreeNode? node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid UpdateHeight(TreeNode node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n
    avl_tree.go
    /* Get node height */\nfunc (t *aVLTree) height(node *TreeNode) int {\n    // Empty node height is -1, leaf node height is 0\n    if node != nil {\n        return node.Height\n    }\n    return -1\n}\n\n/* Update node height */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n    lh := t.height(node.Left)\n    rh := t.height(node.Right)\n    // Node height equals the height of the tallest subtree + 1\n    if lh > rh {\n        node.Height = lh + 1\n    } else {\n        node.Height = rh + 1\n    }\n}\n
    avl_tree.swift
    /* Get node height */\nfunc height(node: TreeNode?) -> Int {\n    // Empty node height is -1, leaf node height is 0\n    node?.height ?? -1\n}\n\n/* Update node height */\nfunc updateHeight(node: TreeNode?) {\n    // Node height equals the height of the tallest subtree + 1\n    node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n
    avl_tree.js
    /* Get node height */\nheight(node) {\n    // Empty node height is -1, leaf node height is 0\n    return node === null ? -1 : node.height;\n}\n\n/* Update node height */\n#updateHeight(node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
    avl_tree.ts
    /* Get node height */\nheight(node: TreeNode): number {\n    // Empty node height is -1, leaf node height is 0\n    return node === null ? -1 : node.height;\n}\n\n/* Update node height */\nupdateHeight(node: TreeNode): void {\n    // Node height equals the height of the tallest subtree + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
    avl_tree.dart
    /* Get node height */\nint height(TreeNode? node) {\n  // Empty node height is -1, leaf node height is 0\n  return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode? node) {\n  // Node height equals the height of the tallest subtree + 1\n  node!.height = max(height(node.left), height(node.right)) + 1;\n}\n
    avl_tree.rs
    /* Get node height */\nfn height(node: OptionTreeNodeRc) -> i32 {\n    // Empty node height is -1, leaf node height is 0\n    match node {\n        Some(node) => node.borrow().height,\n        None => -1,\n    }\n}\n\n/* Update node height */\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        // Node height equals the height of the tallest subtree + 1\n        node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n    }\n}\n
    avl_tree.c
    /* Get node height */\nint height(TreeNode *node) {\n    // Empty node height is -1, leaf node height is 0\n    if (node != NULL) {\n        return node->height;\n    }\n    return -1;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode *node) {\n    int lh = height(node->left);\n    int rh = height(node->right);\n    // Node height equals the height of the tallest subtree + 1\n    if (lh > rh) {\n        node->height = lh + 1;\n    } else {\n        node->height = rh + 1;\n    }\n}\n
    avl_tree.kt
    /* Get node height */\nfun height(node: TreeNode?): Int {\n    // Empty node height is -1, leaf node height is 0\n    return node?.height ?: -1\n}\n\n/* Update node height */\nfun updateHeight(node: TreeNode?) {\n    // Node height equals the height of the tallest subtree + 1\n    node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n
    avl_tree.rb
    ### Get node height ###\ndef height(node)\n  # Empty node height is -1, leaf node height is 0\n  return node.height unless node.nil?\n\n  -1\nend\n\n### Update node height ###\ndef update_height(node)\n  # Node height equals the height of the tallest subtree + 1\n  node.height = [height(node.left), height(node.right)].max + 1\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-node-balance-factor","level":3,"title":"2.   Node Balance Factor","text":"

    The balance factor of a node is defined as the height of the node's left subtree minus the height of its right subtree, and the balance factor of a null node is defined as \\(0\\). We also encapsulate the function to obtain the node's balance factor for convenient subsequent use:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def balance_factor(self, node: TreeNode | None) -> int:\n    \"\"\"Get balance factor\"\"\"\n    # Empty node balance factor is 0\n    if node is None:\n        return 0\n    # Node balance factor = left subtree height - right subtree height\n    return self.height(node.left) - self.height(node.right)\n
    avl_tree.cpp
    /* Get balance factor */\nint balanceFactor(TreeNode *node) {\n    // Empty node balance factor is 0\n    if (node == nullptr)\n        return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return height(node->left) - height(node->right);\n}\n
    avl_tree.java
    /* Get balance factor */\nint balanceFactor(TreeNode node) {\n    // Empty node balance factor is 0\n    if (node == null)\n        return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return height(node.left) - height(node.right);\n}\n
    avl_tree.cs
    /* Get balance factor */\nint BalanceFactor(TreeNode? node) {\n    // Empty node balance factor is 0\n    if (node == null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return Height(node.left) - Height(node.right);\n}\n
    avl_tree.go
    /* Get balance factor */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n    // Empty node balance factor is 0\n    if node == nil {\n        return 0\n    }\n    // Node balance factor = left subtree height - right subtree height\n    return t.height(node.Left) - t.height(node.Right)\n}\n
    avl_tree.swift
    /* Get balance factor */\nfunc balanceFactor(node: TreeNode?) -> Int {\n    // Empty node balance factor is 0\n    guard let node = node else { return 0 }\n    // Node balance factor = left subtree height - right subtree height\n    return height(node: node.left) - height(node: node.right)\n}\n
    avl_tree.js
    /* Get balance factor */\nbalanceFactor(node) {\n    // Empty node balance factor is 0\n    if (node === null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return this.height(node.left) - this.height(node.right);\n}\n
    avl_tree.ts
    /* Get balance factor */\nbalanceFactor(node: TreeNode): number {\n    // Empty node balance factor is 0\n    if (node === null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return this.height(node.left) - this.height(node.right);\n}\n
    avl_tree.dart
    /* Get balance factor */\nint balanceFactor(TreeNode? node) {\n  // Empty node balance factor is 0\n  if (node == null) return 0;\n  // Node balance factor = left subtree height - right subtree height\n  return height(node.left) - height(node.right);\n}\n
    avl_tree.rs
    /* Get balance factor */\nfn balance_factor(node: OptionTreeNodeRc) -> i32 {\n    match node {\n        // Empty node balance factor is 0\n        None => 0,\n        // Node balance factor = left subtree height - right subtree height\n        Some(node) => {\n            Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n        }\n    }\n}\n
    avl_tree.c
    /* Get balance factor */\nint balanceFactor(TreeNode *node) {\n    // Empty node balance factor is 0\n    if (node == NULL) {\n        return 0;\n    }\n    // Node balance factor = left subtree height - right subtree height\n    return height(node->left) - height(node->right);\n}\n
    avl_tree.kt
    /* Get balance factor */\nfun balanceFactor(node: TreeNode?): Int {\n    // Empty node balance factor is 0\n    if (node == null) return 0\n    // Node balance factor = left subtree height - right subtree height\n    return height(node.left) - height(node.right)\n}\n
    avl_tree.rb
    ### Get balance factor ###\ndef balance_factor(node)\n  # Empty node balance factor is 0\n  return 0 if node.nil?\n\n  # Node balance factor = left subtree height - right subtree height\n  height(node.left) - height(node.right)\nend\n

    Tip

    Let the balance factor be \\(f\\), then the balance factor of any node in an AVL tree satisfies \\(-1 \\le f \\le 1\\).

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-rotations-in-avl-trees","level":2,"title":"7.5.2   Rotations in AVL Trees","text":"

    The characteristic of AVL trees lies in the \"rotation\" operation, which can restore balance to unbalanced nodes without affecting the inorder traversal sequence of the binary tree. In other words, rotation operations can both maintain the property of a \"binary search tree\" and make the tree return to a \"balanced binary tree\".

    We call nodes with a balance factor absolute value \\(> 1\\) \"unbalanced nodes\". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, right rotation then left rotation, and left rotation then right rotation. Below we describe these rotation operations in detail.

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-right-rotation","level":3,"title":"1.   Right Rotation","text":"

    As shown in Figure 7-26, the value below the node is the balance factor. From bottom to top, the first unbalanced node in the binary tree is \"node 3\". We focus on the subtree with this unbalanced node as the root, denoting the node as node and its left child as child, and perform a \"right rotation\" operation. After the right rotation is completed, the subtree regains balance and still maintains the properties of a binary search tree.

    <1><2><3><4>

    Figure 7-26   Steps of right rotation

    As shown in Figure 7-27, when the child node has a right child (denoted as grand_child), a step needs to be added in the right rotation: set grand_child as the left child of node.

    Figure 7-27   Right rotation with grand_child

    \"Right rotation\" is a figurative term; in practice, it is achieved by modifying node pointers, as shown in the following code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Right rotation operation\"\"\"\n    child = node.left\n    grand_child = child.right\n    # Using child as pivot, rotate node to the right\n    child.right = node\n    node.left = grand_child\n    # Update node height\n    self.update_height(node)\n    self.update_height(child)\n    # Return root node of subtree after rotation\n    return child\n
    avl_tree.cpp
    /* Right rotation operation */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child = node->left;\n    TreeNode *grandChild = child->right;\n    // Using child as pivot, rotate node to the right\n    child->right = node;\n    node->left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.java
    /* Right rotation operation */\nTreeNode rightRotate(TreeNode node) {\n    TreeNode child = node.left;\n    TreeNode grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.cs
    /* Right rotation operation */\nTreeNode? RightRotate(TreeNode? node) {\n    TreeNode? child = node?.left;\n    TreeNode? grandChild = child?.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.go
    /* Right rotation operation */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n    child := node.Left\n    grandChild := child.Right\n    // Using child as pivot, rotate node to the right\n    child.Right = node\n    node.Left = grandChild\n    // Update node height\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.swift
    /* Right rotation operation */\nfunc rightRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.left\n    let grandChild = child?.right\n    // Using child as pivot, rotate node to the right\n    child?.right = node\n    node?.left = grandChild\n    // Update node height\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.js
    /* Right rotation operation */\n#rightRotate(node) {\n    const child = node.left;\n    const grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.ts
    /* Right rotation operation */\nrightRotate(node: TreeNode): TreeNode {\n    const child = node.left;\n    const grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.dart
    /* Right rotation operation */\nTreeNode? rightRotate(TreeNode? node) {\n  TreeNode? child = node!.left;\n  TreeNode? grandChild = child!.right;\n  // Using child as pivot, rotate node to the right\n  child.right = node;\n  node.left = grandChild;\n  // Update node height\n  updateHeight(node);\n  updateHeight(child);\n  // Return root node of subtree after rotation\n  return child;\n}\n
    avl_tree.rs
    /* Right rotation operation */\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            // Using child as pivot, rotate node to the right\n            child.borrow_mut().right = Some(node.clone());\n            node.borrow_mut().left = grand_child;\n            // Update node height\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Return root node of subtree after rotation\n            Some(child)\n        }\n        None => None,\n    }\n}\n
    avl_tree.c
    /* Right rotation operation */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->left;\n    grandChild = child->right;\n    // Using child as pivot, rotate node to the right\n    child->right = node;\n    node->left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.kt
    /* Right rotation operation */\nfun rightRotate(node: TreeNode?): TreeNode {\n    val child = node!!.left\n    val grandChild = child!!.right\n    // Using child as pivot, rotate node to the right\n    child.right = node\n    node.left = grandChild\n    // Update node height\n    updateHeight(node)\n    updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.rb
    ### Right rotation ###\ndef right_rotate(node)\n  child = node.left\n  grand_child = child.right\n  # Using child as pivot, rotate node to the right\n  child.right = node\n  node.left = grand_child\n  # Update node height\n  update_height(node)\n  update_height(child)\n  # Return root node of subtree after rotation\n  child\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-left-rotation","level":3,"title":"2.   Left Rotation","text":"

    Correspondingly, if considering the \"mirror\" of the above unbalanced binary tree, the \"left rotation\" operation shown in Figure 7-28 needs to be performed.

    Figure 7-28   Left rotation operation

    Similarly, as shown in Figure 7-29, when the child node has a left child (denoted as grand_child), a step needs to be added in the left rotation: set grand_child as the right child of node.

    Figure 7-29   Left rotation with grand_child

    It can be observed that right rotation and left rotation operations are mirror symmetric in logic, and the two imbalance cases they solve are also symmetric. Based on symmetry, we only need to replace all left in the right rotation implementation code with right, and all right with left, to obtain the left rotation implementation code:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Left rotation operation\"\"\"\n    child = node.right\n    grand_child = child.left\n    # Using child as pivot, rotate node to the left\n    child.left = node\n    node.right = grand_child\n    # Update node height\n    self.update_height(node)\n    self.update_height(child)\n    # Return root node of subtree after rotation\n    return child\n
    avl_tree.cpp
    /* Left rotation operation */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child = node->right;\n    TreeNode *grandChild = child->left;\n    // Using child as pivot, rotate node to the left\n    child->left = node;\n    node->right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.java
    /* Left rotation operation */\nTreeNode leftRotate(TreeNode node) {\n    TreeNode child = node.right;\n    TreeNode grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.cs
    /* Left rotation operation */\nTreeNode? LeftRotate(TreeNode? node) {\n    TreeNode? child = node?.right;\n    TreeNode? grandChild = child?.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.go
    /* Left rotation operation */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n    child := node.Right\n    grandChild := child.Left\n    // Using child as pivot, rotate node to the left\n    child.Left = node\n    node.Right = grandChild\n    // Update node height\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.swift
    /* Left rotation operation */\nfunc leftRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.right\n    let grandChild = child?.left\n    // Using child as pivot, rotate node to the left\n    child?.left = node\n    node?.right = grandChild\n    // Update node height\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.js
    /* Left rotation operation */\n#leftRotate(node) {\n    const child = node.right;\n    const grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.ts
    /* Left rotation operation */\nleftRotate(node: TreeNode): TreeNode {\n    const child = node.right;\n    const grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.dart
    /* Left rotation operation */\nTreeNode? leftRotate(TreeNode? node) {\n  TreeNode? child = node!.right;\n  TreeNode? grandChild = child!.left;\n  // Using child as pivot, rotate node to the left\n  child.left = node;\n  node.right = grandChild;\n  // Update node height\n  updateHeight(node);\n  updateHeight(child);\n  // Return root node of subtree after rotation\n  return child;\n}\n
    avl_tree.rs
    /* Left rotation operation */\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            // Using child as pivot, rotate node to the left\n            child.borrow_mut().left = Some(node.clone());\n            node.borrow_mut().right = grand_child;\n            // Update node height\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Return root node of subtree after rotation\n            Some(child)\n        }\n        None => None,\n    }\n}\n
    avl_tree.c
    /* Left rotation operation */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->right;\n    grandChild = child->left;\n    // Using child as pivot, rotate node to the left\n    child->left = node;\n    node->right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
    avl_tree.kt
    /* Left rotation operation */\nfun leftRotate(node: TreeNode?): TreeNode {\n    val child = node!!.right\n    val grandChild = child!!.left\n    // Using child as pivot, rotate node to the left\n    child.left = node\n    node.right = grandChild\n    // Update node height\n    updateHeight(node)\n    updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
    avl_tree.rb
    ### Left rotation ###\ndef left_rotate(node)\n  child = node.right\n  grand_child = child.left\n  # Using child as pivot, rotate node to the left\n  child.left = node\n  node.right = grand_child\n  # Update node height\n  update_height(node)\n  update_height(child)\n  # Return root node of subtree after rotation\n  child\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3-left-rotation-then-right-rotation","level":3,"title":"3.   Left Rotation Then Right Rotation","text":"

    For the unbalanced node 3 in Figure 7-30, using either left rotation or right rotation alone cannot restore the subtree to balance. In this case, a \"left rotation\" needs to be performed on child first, followed by a \"right rotation\" on node.

    Figure 7-30   Left-right rotation

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4-right-rotation-then-left-rotation","level":3,"title":"4.   Right Rotation Then Left Rotation","text":"

    As shown in Figure 7-31, for the mirror case of the above unbalanced binary tree, a \"right rotation\" needs to be performed on child first, then a \"left rotation\" on node.

    Figure 7-31   Right-left rotation

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5-choice-of-rotation","level":3,"title":"5.   Choice of Rotation","text":"

    The four imbalances shown in Figure 7-32 correspond one-to-one with the above cases, requiring right rotation, left rotation then right rotation, right rotation then left rotation, and left rotation operations respectively.

    Figure 7-32   The four rotation cases of AVL tree

    As shown in Table 7-3, we determine which case the unbalanced node belongs to by judging the signs of the balance factor of the unbalanced node and the balance factor of its taller-side child node.

    Table 7-3   Conditions for Choosing Among the Four Rotation Cases

    Balance factor of the unbalanced node Balance factor of the child node Rotation method to apply \\(> 1\\) (left-leaning tree) \\(\\geq 0\\) Right rotation \\(> 1\\) (left-leaning tree) \\(<0\\) Left rotation then right rotation \\(< -1\\) (right-leaning tree) \\(\\leq 0\\) Left rotation \\(< -1\\) (right-leaning tree) \\(>0\\) Right rotation then left rotation

    For ease of use, we encapsulate the rotation operations into a function. With this function, we can perform rotations for various imbalance situations, restoring balance to unbalanced nodes. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Perform rotation operation to restore balance to this subtree\"\"\"\n    # Get balance factor of node\n    balance_factor = self.balance_factor(node)\n    # Left-leaning tree\n    if balance_factor > 1:\n        if self.balance_factor(node.left) >= 0:\n            # Right rotation\n            return self.right_rotate(node)\n        else:\n            # First left rotation then right rotation\n            node.left = self.left_rotate(node.left)\n            return self.right_rotate(node)\n    # Right-leaning tree\n    elif balance_factor < -1:\n        if self.balance_factor(node.right) <= 0:\n            # Left rotation\n            return self.left_rotate(node)\n        else:\n            # First right rotation then left rotation\n            node.right = self.right_rotate(node.right)\n            return self.left_rotate(node)\n    # Balanced tree, no rotation needed, return directly\n    return node\n
    avl_tree.cpp
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode *rotate(TreeNode *node) {\n    // Get balance factor of node\n    int _balanceFactor = balanceFactor(node);\n    // Left-leaning tree\n    if (_balanceFactor > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (_balanceFactor < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.java
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode rotate(TreeNode node) {\n    // Get balance factor of node\n    int balanceFactor = balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = leftRotate(node.left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = rightRotate(node.right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.cs
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode? Rotate(TreeNode? node) {\n    // Get balance factor of node\n    int balanceFactorInt = BalanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactorInt > 1) {\n        if (BalanceFactor(node?.left) >= 0) {\n            // Right rotation\n            return RightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node!.left = LeftRotate(node!.left);\n            return RightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactorInt < -1) {\n        if (BalanceFactor(node?.right) <= 0) {\n            // Left rotation\n            return LeftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node!.right = RightRotate(node!.right);\n            return LeftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.go
    /* Perform rotation operation to restore balance to this subtree */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n    // Get balance factor of node\n    // Go recommends short variables, here bf refers to t.balanceFactor\n    bf := t.balanceFactor(node)\n    // Left-leaning tree\n    if bf > 1 {\n        if t.balanceFactor(node.Left) >= 0 {\n            // Right rotation\n            return t.rightRotate(node)\n        } else {\n            // First left rotation then right rotation\n            node.Left = t.leftRotate(node.Left)\n            return t.rightRotate(node)\n        }\n    }\n    // Right-leaning tree\n    if bf < -1 {\n        if t.balanceFactor(node.Right) <= 0 {\n            // Left rotation\n            return t.leftRotate(node)\n        } else {\n            // First right rotation then left rotation\n            node.Right = t.rightRotate(node.Right)\n            return t.leftRotate(node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
    avl_tree.swift
    /* Perform rotation operation to restore balance to this subtree */\nfunc rotate(node: TreeNode?) -> TreeNode? {\n    // Get balance factor of node\n    let balanceFactor = balanceFactor(node: node)\n    // Left-leaning tree\n    if balanceFactor > 1 {\n        if self.balanceFactor(node: node?.left) >= 0 {\n            // Right rotation\n            return rightRotate(node: node)\n        } else {\n            // First left rotation then right rotation\n            node?.left = leftRotate(node: node?.left)\n            return rightRotate(node: node)\n        }\n    }\n    // Right-leaning tree\n    if balanceFactor < -1 {\n        if self.balanceFactor(node: node?.right) <= 0 {\n            // Left rotation\n            return leftRotate(node: node)\n        } else {\n            // First right rotation then left rotation\n            node?.right = rightRotate(node: node?.right)\n            return leftRotate(node: node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
    avl_tree.js
    /* Perform rotation operation to restore balance to this subtree */\n#rotate(node) {\n    // Get balance factor of node\n    const balanceFactor = this.balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return this.#rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = this.#leftRotate(node.left);\n            return this.#rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return this.#leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = this.#rightRotate(node.right);\n            return this.#leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.ts
    /* Perform rotation operation to restore balance to this subtree */\nrotate(node: TreeNode): TreeNode {\n    // Get balance factor of node\n    const balanceFactor = this.balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return this.rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = this.leftRotate(node.left);\n            return this.rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return this.leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = this.rightRotate(node.right);\n            return this.leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.dart
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode? rotate(TreeNode? node) {\n  // Get balance factor of node\n  int factor = balanceFactor(node);\n  // Left-leaning tree\n  if (factor > 1) {\n    if (balanceFactor(node!.left) >= 0) {\n      // Right rotation\n      return rightRotate(node);\n    } else {\n      // First left rotation then right rotation\n      node.left = leftRotate(node.left);\n      return rightRotate(node);\n    }\n  }\n  // Right-leaning tree\n  if (factor < -1) {\n    if (balanceFactor(node!.right) <= 0) {\n      // Left rotation\n      return leftRotate(node);\n    } else {\n      // First right rotation then left rotation\n      node.right = rightRotate(node.right);\n      return leftRotate(node);\n    }\n  }\n  // Balanced tree, no rotation needed, return directly\n  return node;\n}\n
    avl_tree.rs
    /* Perform rotation operation to restore balance to this subtree */\nfn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    // Get balance factor of node\n    let balance_factor = Self::balance_factor(node.clone());\n    // Left-leaning tree\n    if balance_factor > 1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().left.clone()) >= 0 {\n            // Right rotation\n            Self::right_rotate(Some(node))\n        } else {\n            // First left rotation then right rotation\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    // Right-leaning tree\n    else if balance_factor < -1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().right.clone()) <= 0 {\n            // Left rotation\n            Self::left_rotate(Some(node))\n        } else {\n            // First right rotation then left rotation\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        // Balanced tree, no rotation needed, return directly\n        node\n    }\n}\n
    avl_tree.c
    /* Perform rotation operation to restore balance to this subtree */\nTreeNode *rotate(TreeNode *node) {\n    // Get balance factor of node\n    int bf = balanceFactor(node);\n    // Left-leaning tree\n    if (bf > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (bf < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
    avl_tree.kt
    /* Perform rotation operation to restore balance to this subtree */\nfun rotate(node: TreeNode): TreeNode {\n    // Get balance factor of node\n    val balanceFactor = balanceFactor(node)\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return rightRotate(node)\n        } else {\n            // First left rotation then right rotation\n            node.left = leftRotate(node.left)\n            return rightRotate(node)\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return leftRotate(node)\n        } else {\n            // First right rotation then left rotation\n            node.right = rightRotate(node.right)\n            return leftRotate(node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
    avl_tree.rb
    ### Perform rotation to rebalance subtree ###\ndef rotate(node)\n  # Get balance factor of node\n  balance_factor = balance_factor(node)\n  # Left-heavy tree\n  if balance_factor > 1\n    if balance_factor(node.left) >= 0\n      # Right rotation\n      return right_rotate(node)\n    else\n      # First left rotation then right rotation\n      node.left = left_rotate(node.left)\n      return right_rotate(node)\n    end\n  # Right-heavy tree\n  elsif balance_factor < -1\n    if balance_factor(node.right) <= 0\n      # Left rotation\n      return left_rotate(node)\n    else\n      # First right rotation then left rotation\n      node.right = right_rotate(node.right)\n      return left_rotate(node)\n    end\n  end\n  # Balanced tree, no rotation needed, return directly\n  node\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-common-operations-in-avl-trees","level":2,"title":"7.5.3   Common Operations in AVL Trees","text":"","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-node-insertion","level":3,"title":"1.   Node Insertion","text":"

    The node insertion operation in AVL trees is similar in principle to that in binary search trees. The only difference is that after inserting a node in an AVL tree, a series of unbalanced nodes may appear on the path from that node to the root. Therefore, we need to start from this node and perform rotation operations from bottom to top, restoring balance to all unbalanced nodes. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def insert(self, val):\n    \"\"\"Insert node\"\"\"\n    self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n    \"\"\"Recursively insert node (helper method)\"\"\"\n    if node is None:\n        return TreeNode(val)\n    # 1. Find insertion position and insert node\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        # Duplicate node not inserted, return directly\n        return node\n    # Update node height\n    self.update_height(node)\n    # 2. Perform rotation operation to restore balance to this subtree\n    return self.rotate(node)\n
    avl_tree.cpp
    /* Insert node */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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;    // Duplicate node not inserted, return directly\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.java
    /* Insert node */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode insertHelper(TreeNode node, int val) {\n    if (node == null)\n        return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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; // Duplicate node not inserted, return directly\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.cs
    /* Insert node */\nvoid Insert(int val) {\n    root = InsertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n    if (node == null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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;     // Duplicate node not inserted, return directly\n    UpdateHeight(node);  // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = Rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.go
    /* Insert node */\nfunc (t *aVLTree) insert(val int) {\n    t.root = t.insertHelper(t.root, val)\n}\n\n/* Recursively insert node (helper function) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return NewTreeNode(val)\n    }\n    /* 1. Find insertion position and insert node */\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        // Duplicate node not inserted, return directly\n        return node\n    }\n    // Update node height\n    t.updateHeight(node)\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = t.rotate(node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.swift
    /* Insert node */\nfunc insert(val: Int) {\n    root = insertHelper(node: root, val: val)\n}\n\n/* Recursively insert node (helper method) */\nfunc insertHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return TreeNode(x: val)\n    }\n    /* 1. Find insertion position and insert node */\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 // Duplicate node not inserted, return directly\n    }\n    updateHeight(node: node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node: node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.js
    /* Insert node */\ninsert(val) {\n    this.root = this.#insertHelper(this.root, val);\n}\n\n/* Recursively insert node (helper method) */\n#insertHelper(node, val) {\n    if (node === null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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; // Duplicate node not inserted, return directly\n    this.#updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.#rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.ts
    /* Insert node */\ninsert(val: number): void {\n    this.root = this.insertHelper(this.root, val);\n}\n\n/* Recursively insert node (helper method) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\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; // Duplicate node not inserted, return directly\n    }\n    this.updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.dart
    /* Insert node */\nvoid insert(int val) {\n  root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n  if (node == null) return TreeNode(val);\n  /* 1. Find insertion position and insert node */\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; // Duplicate node not inserted, return directly\n  updateHeight(node); // Update node height\n  /* 2. Perform rotation operation to restore balance to this subtree */\n  node = rotate(node);\n  // Return root node of subtree\n  return node;\n}\n
    avl_tree.rs
    /* Insert node */\nfn insert(&mut self, val: i32) {\n    self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* Recursively insert node (helper method) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Find insertion position and insert node */\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); // Duplicate node not inserted, return directly\n                }\n            }\n            Self::update_height(Some(node.clone())); // Update node height\n\n            /* 2. Perform rotation operation to restore balance to this subtree */\n            node = Self::rotate(Some(node)).unwrap();\n            // Return root node of subtree\n            Some(node)\n        }\n        None => Some(TreeNode::new(val)),\n    }\n}\n
    avl_tree.c
    /* Insert node */\nvoid insert(AVLTree *tree, int val) {\n    tree->root = insertHelper(tree->root, val);\n}\n\n/* Recursively insert node (helper function) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == NULL) {\n        return newTreeNode(val);\n    }\n    /* 1. Find insertion position and insert node */\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        // Duplicate node not inserted, return directly\n        return node;\n    }\n    // Update node height\n    updateHeight(node);\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.kt
    /* Insert node */\nfun insert(_val: Int) {\n    root = insertHelper(root, _val)\n}\n\n/* Recursively insert node (helper method) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n    if (n == null)\n        return TreeNode(_val)\n    var node = n\n    /* 1. Find insertion position and insert node */\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 // Duplicate node not inserted, return directly\n    updateHeight(node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.rb
    ### Insert node ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n### Recursively insert node (helper method) ###\ndef insert_helper(node, val)\n  return TreeNode.new(val) if node.nil?\n  # 1. Find insertion position and insert node\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    # Duplicate node not inserted, return directly\n    return node\n  end\n  # Update node height\n  update_height(node)\n  # 2. Perform rotation operation to restore balance to this subtree\n  rotate(node)\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-node-removal","level":3,"title":"2.   Node Removal","text":"

    Similarly, on the basis of the binary search tree's node removal method, rotation operations need to be performed from bottom to top to restore balance to all unbalanced nodes. The code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
    def remove(self, val: int):\n    \"\"\"Delete node\"\"\"\n    self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n    \"\"\"Recursively delete node (helper method)\"\"\"\n    if node is None:\n        return None\n    # 1. Find node and delete\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            # Number of child nodes = 0, delete node directly and return\n            if child is None:\n                return None\n            # Number of child nodes = 1, delete node directly\n            else:\n                node = child\n        else:\n            # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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    # Update node height\n    self.update_height(node)\n    # 2. Perform rotation operation to restore balance to this subtree\n    return self.rotate(node)\n
    avl_tree.cpp
    /* Remove node */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return nullptr;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == nullptr) {\n                delete node;\n                return nullptr;\n            }\n            // Number of child nodes = 1, delete node directly\n            else {\n                delete node;\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.java
    /* Remove node */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode removeHelper(TreeNode node, int val) {\n    if (node == null)\n        return null;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null;\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.cs
    /* Remove node */\nvoid Remove(int val) {\n    root = RemoveHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n    if (node == null) return null;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null;\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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);  // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = Rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.go
    /* Remove node */\nfunc (t *aVLTree) remove(val int) {\n    t.root = t.removeHelper(t.root, val)\n}\n\n/* Recursively remove node (helper function) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return nil\n    }\n    /* 1. Find node and delete */\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                // Number of child nodes = 0, delete node directly and return\n                return nil\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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    // Update node height\n    t.updateHeight(node)\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = t.rotate(node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.swift
    /* Remove node */\nfunc remove(val: Int) {\n    root = removeHelper(node: root, val: val)\n}\n\n/* Recursively delete node (helper method) */\nfunc removeHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return nil\n    }\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if child == nil {\n                return nil\n            }\n            // Number of child nodes = 1, delete node directly\n            else {\n                node = child\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node: node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.js
    /* Remove node */\nremove(val) {\n    this.root = this.#removeHelper(this.root, val);\n}\n\n/* Recursively delete node (helper method) */\n#removeHelper(node, val) {\n    if (node === null) return null;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child === null) return null;\n            // Number of child nodes = 1, delete node directly\n            else node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.#rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.ts
    /* Remove node */\nremove(val: number): void {\n    this.root = this.removeHelper(this.root, val);\n}\n\n/* Recursively delete node (helper method) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return null;\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child === null) {\n                return null;\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.dart
    /* Remove node */\nvoid remove(int val) {\n  root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n  if (node == null) return null;\n  /* 1. Find node and delete */\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      // Number of child nodes = 0, delete node directly and return\n      if (child == null)\n        return null;\n      // Number of child nodes = 1, delete node directly\n      else\n        node = child;\n    } else {\n      // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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); // Update node height\n  /* 2. Perform rotation operation to restore balance to this subtree */\n  node = rotate(node);\n  // Return root node of subtree\n  return node;\n}\n
    avl_tree.rs
    /* Remove node */\nfn remove(&self, val: i32) {\n    Self::remove_helper(self.root.clone(), val);\n}\n\n/* Recursively delete node (helper method) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Find node and delete */\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                    // Number of child nodes = 0, delete node directly and return\n                    None => {\n                        return None;\n                    }\n                    // Number of child nodes = 1, delete node directly\n                    Some(child) => node = child,\n                }\n            } else {\n                // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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())); // Update node height\n\n            /* 2. Perform rotation operation to restore balance to this subtree */\n            node = Self::rotate(Some(node)).unwrap();\n            // Return root node of subtree\n            Some(node)\n        }\n        None => None,\n    }\n}\n
    avl_tree.c
    /* Remove node */\n// Cannot use remove keyword here due to stdio.h inclusion\nvoid removeItem(AVLTree *tree, int val) {\n    TreeNode *root = removeHelper(tree->root, val);\n}\n\n/* Recursively remove node (helper function) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    TreeNode *child, *grandChild;\n    if (node == NULL) {\n        return NULL;\n    }\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == NULL) {\n                return NULL;\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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    // Update node height\n    updateHeight(node);\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
    avl_tree.kt
    /* Remove node */\nfun remove(_val: Int) {\n    root = removeHelper(root, _val)\n}\n\n/* Recursively delete node (helper method) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n    var node = n ?: return null\n    /* 1. Find node and delete */\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            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node)\n    // Return root node of subtree\n    return node\n}\n
    avl_tree.rb
    ### Delete node ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n### Recursively delete node (helper method) ###\ndef remove_helper(node, val)\n  return if node.nil?\n  # 1. Find node and delete\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      # Number of child nodes = 0, delete node directly and return\n      return if child.nil?\n      # Number of child nodes = 1, delete node directly\n      node = child\n    else\n      # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\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  # Update node height\n  update_height(node)\n  # 2. Perform rotation operation to restore balance to this subtree\n  rotate(node)\nend\n
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3-node-search","level":3,"title":"3.   Node Search","text":"

    The node search operation in AVL trees is consistent with that in binary search trees, and will not be elaborated here.

    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-typical-applications-of-avl-trees","level":2,"title":"7.5.4   Typical Applications of AVL Trees","text":"
    • Organizing and storing large-scale data, suitable for scenarios with high-frequency searches and low-frequency insertions and deletions.
    • Used to build index systems in databases.
    • Red-black trees are also a common type of balanced binary search tree. Compared to AVL trees, red-black trees have more relaxed balance conditions, require fewer rotation operations for node insertion and deletion, and have higher average efficiency for node addition and deletion operations.
    ","path":["Chapter 7. Tree","7.5   AVL Tree *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   Binary Search Tree","text":"

    As shown in Figure 7-16, a binary search tree satisfies the following conditions.

    1. For the root node, the value of all nodes in the left subtree \\(<\\) the value of the root node \\(<\\) the value of all nodes in the right subtree.
    2. The left and right subtrees of any node are also binary search trees, i.e., they satisfy condition 1. as well.

    Figure 7-16   Binary search tree

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741-operations-on-a-binary-search-tree","level":2,"title":"7.4.1   Operations on a Binary Search Tree","text":"

    We encapsulate the binary search tree as a class BinarySearchTree and declare a member variable root pointing to the tree's root node.

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1-searching-for-a-node","level":3,"title":"1.   Searching for a Node","text":"

    Given a target node value num, we can search according to the properties of the binary search tree. As shown in Figure 7-17, we declare a node cur and start from the binary search tree's root node root, looping to compare cur.val with num.

    • If cur.val < num, it means the target node is in cur's right subtree, thus execute cur = cur.right.
    • If cur.val > num, it means the target node is in cur's left subtree, thus execute cur = cur.left.
    • If cur.val = num, it means the target node is found, exit the loop, and return the node.
    <1><2><3><4>

    Figure 7-17   Example of searching for a node in a binary search tree

    The search operation in a binary search tree follows the same principle as binary search: each round rules out half of the remaining cases. The number of loop iterations is at most the height of the tree. When the tree is balanced, the search takes \\(O(\\log n)\\) time. The example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
    def search(self, num: int) -> TreeNode | None:\n    \"\"\"Search node\"\"\"\n    cur = self._root\n    # Loop search, exit after passing leaf node\n    while cur is not None:\n        # Target node is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Target node is in cur's left subtree\n        elif cur.val > num:\n            cur = cur.left\n        # Found target node, exit loop\n        else:\n            break\n    return cur\n
    binary_search_tree.cpp
    /* Search node */\nTreeNode *search(int num) {\n    TreeNode *cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Target node is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Target node is in cur's left subtree\n        else if (cur->val > num)\n            cur = cur->left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.java
    /* Search node */\nTreeNode search(int num) {\n    TreeNode cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num)\n            cur = cur.left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.cs
    /* Search node */\nTreeNode? Search(int num) {\n    TreeNode? cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur =\n            cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num)\n            cur = cur.left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.go
    /* Search node */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n    node := bst.root\n    // Loop search, exit after passing leaf node\n    for node != nil {\n        if node.Val.(int) < num {\n            // Target node is in cur's right subtree\n            node = node.Right\n        } else if node.Val.(int) > num {\n            // Target node is in cur's left subtree\n            node = node.Left\n        } else {\n            // Found target node, exit loop\n            break\n        }\n    }\n    // Return target node\n    return node\n}\n
    binary_search_tree.swift
    /* Search node */\nfunc search(num: Int) -> TreeNode? {\n    var cur = root\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Target node is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Target node is in cur's left subtree\n        else if cur!.val > num {\n            cur = cur?.left\n        }\n        // Found target node, exit loop\n        else {\n            break\n        }\n    }\n    // Return target node\n    return cur\n}\n
    binary_search_tree.js
    /* Search node */\nsearch(num) {\n    let cur = this.root;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num) cur = cur.left;\n        // Found target node, exit loop\n        else break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.ts
    /* Search node */\nsearch(num: number): TreeNode | null {\n    let cur = this.root;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num) cur = cur.left;\n        // Found target node, exit loop\n        else break;\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.dart
    /* Search node */\nTreeNode? search(int _num) {\n  TreeNode? cur = _root;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Target node is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Target node is in cur's left subtree\n    else if (cur.val > _num)\n      cur = cur.left;\n    // Found target node, exit loop\n    else\n      break;\n  }\n  // Return target node\n  return cur;\n}\n
    binary_search_tree.rs
    /* Search node */\npub fn search(&self, num: i32) -> OptionTreeNodeRc {\n    let mut cur = self.root.clone();\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Target node is in cur's right subtree\n            Ordering::Greater => cur = node.borrow().right.clone(),\n            // Target node is in cur's left subtree\n            Ordering::Less => cur = node.borrow().left.clone(),\n            // Found target node, exit loop\n            Ordering::Equal => break,\n        }\n    }\n\n    // Return target node\n    cur\n}\n
    binary_search_tree.c
    /* Search node */\nTreeNode *search(BinarySearchTree *bst, int num) {\n    TreeNode *cur = bst->root;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        if (cur->val < num) {\n            // Target node is in cur's right subtree\n            cur = cur->right;\n        } else if (cur->val > num) {\n            // Target node is in cur's left subtree\n            cur = cur->left;\n        } else {\n            // Found target node, exit loop\n            break;\n        }\n    }\n    // Return target node\n    return cur;\n}\n
    binary_search_tree.kt
    /* Search node */\nfun search(num: Int): TreeNode? {\n    var cur = root\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Target node is in cur's left subtree\n        else if (cur._val > num)\n            cur.left\n        // Found target node, exit loop\n        else\n            break\n    }\n    // Return target node\n    return cur\n}\n
    binary_search_tree.rb
    ### Search node ###\ndef search(num)\n  cur = @root\n\n  # Loop search, exit after passing leaf node\n  while !cur.nil?\n    # Target node is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Target node is in cur's left subtree\n    elsif cur.val > num\n      cur = cur.left\n    # Found target node, exit loop\n    else\n      break\n    end\n  end\n\n  cur\nend\n
    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2-inserting-a-node","level":3,"title":"2.   Inserting a Node","text":"

    Given an element num to be inserted, in order to maintain the property of the binary search tree \"left subtree < root node < right subtree,\" the insertion process is as shown in Figure 7-18.

    1. Finding the insertion position: Similar to the search operation, start from the root node and loop downward searching according to the size relationship between the current node value and num, until passing the leaf node (traversing to None) and then exit the loop.
    2. Insert the node at that position: Create a node for num and place it at the None position.

    Figure 7-18   Inserting a node into a binary search tree

    In the code implementation, note the following two points:

    • Binary search trees do not allow duplicate nodes; otherwise, the tree would no longer satisfy its definition. Therefore, if the node to be inserted already exists in the tree, the insertion is skipped and the function returns directly.
    • To implement the node insertion, we need to use node pre to save the node from the previous loop iteration. This way, when traversing to None, we can obtain its parent node, thereby completing the node insertion operation.
    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
    def insert(self, num: int):\n    \"\"\"Insert node\"\"\"\n    # If tree is empty, initialize root node\n    if self._root is None:\n        self._root = TreeNode(num)\n        return\n    # Loop search, exit after passing leaf node\n    cur, pre = self._root, None\n    while cur is not None:\n        # Found duplicate node, return directly\n        if cur.val == num:\n            return\n        pre = cur\n        # Insertion position is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Insertion position is in cur's left subtree\n        else:\n            cur = cur.left\n    # Insert node\n    node = TreeNode(num)\n    if pre.val < num:\n        pre.right = node\n    else:\n        pre.left = node\n
    binary_search_tree.cpp
    /* Insert node */\nvoid insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == nullptr) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode *cur = root, *pre = nullptr;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Found duplicate node, return directly\n        if (cur->val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur->left;\n    }\n    // Insert node\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
    /* Insert node */\nvoid insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // Insert node\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
    /* Insert node */\nvoid Insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode? cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n\n    // Insert node\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
    /* Insert node */\nfunc (bst *binarySearchTree) insert(num int) {\n    cur := bst.root\n    // If tree is empty, initialize root node\n    if cur == nil {\n        bst.root = NewTreeNode(num)\n        return\n    }\n    // Node position before the node to be inserted\n    var pre *TreeNode = nil\n    // Loop search, exit after passing leaf node\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    // Insert node\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
    /* Insert node */\nfunc insert(num: Int) {\n    // If tree is empty, initialize root node\n    if root == nil {\n        root = TreeNode(x: num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Found duplicate node, return directly\n        if cur!.val == num {\n            return\n        }\n        pre = cur\n        // Insertion position is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Insertion position is in cur's left subtree\n        else {\n            cur = cur?.left\n        }\n    }\n    // Insert node\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
    /* Insert node */\ninsert(num) {\n    // If tree is empty, initialize root node\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur = this.root,\n        pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found duplicate node, return directly\n        if (cur.val === num) return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else cur = cur.left;\n    }\n    // Insert node\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
    /* Insert node */\ninsert(num: number): void {\n    // If tree is empty, initialize root node\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    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found duplicate node, return directly\n        if (cur.val === num) return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else cur = cur.left;\n    }\n    // Insert node\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
    /* Insert node */\nvoid insert(int _num) {\n  // If tree is empty, initialize root node\n  if (_root == null) {\n    _root = TreeNode(_num);\n    return;\n  }\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Found duplicate node, return directly\n    if (cur.val == _num) return;\n    pre = cur;\n    // Insertion position is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Insertion position is in cur's left subtree\n    else\n      cur = cur.left;\n  }\n  // Insert node\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
    /* Insert node */\npub fn insert(&mut self, num: i32) {\n    // If tree is empty, initialize root node\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    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Found duplicate node, return directly\n            Ordering::Equal => return,\n            // Insertion position is in cur's right subtree\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Insertion position is in cur's left subtree\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // Insert node\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
    /* Insert node */\nvoid insert(BinarySearchTree *bst, int num) {\n    // If tree is empty, initialize root node\n    if (bst->root == NULL) {\n        bst->root = newTreeNode(num);\n        return;\n    }\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        // Found duplicate node, return directly\n        if (cur->val == num) {\n            return;\n        }\n        pre = cur;\n        if (cur->val < num) {\n            // Insertion position is in cur's right subtree\n            cur = cur->right;\n        } else {\n            // Insertion position is in cur's left subtree\n            cur = cur->left;\n        }\n    }\n    // Insert node\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
    /* Insert node */\nfun insert(num: Int) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = TreeNode(num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode? = null\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur._val == num)\n            return\n        pre = cur\n        // Insertion position is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Insertion position is in cur's left subtree\n        else\n            cur.left\n    }\n    // Insert node\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
    ### Insert node ###\ndef insert(num)\n  # If tree is empty, initialize root node\n  if @root.nil?\n    @root = TreeNode.new(num)\n    return\n  end\n\n  # Loop search, exit after passing leaf node\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Found duplicate node, return directly\n    return if cur.val == num\n\n    pre = cur\n    # Insertion position is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Insertion position is in cur's left subtree\n    else\n      cur = cur.left\n    end\n  end\n\n  # Insert node\n  node = TreeNode.new(num)\n  if pre.val < num\n    pre.right = node\n  else\n    pre.left = node\n  end\nend\n

    Similar to searching for a node, inserting a node uses \\(O(\\log n)\\) time.

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3-removing-a-node","level":3,"title":"3.   Removing a Node","text":"

    First, find the target node in the binary search tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of \"left subtree \\(<\\) root node \\(<\\) right subtree\" is still maintained. Therefore, depending on the number of child nodes the target node has, we consider three cases: degree \\(0\\), degree \\(1\\), and degree \\(2\\), and perform the corresponding removal operation.

    As shown in Figure 7-19, when the degree of the node to be removed is \\(0\\), it means the node is a leaf node and can be directly removed.

    Figure 7-19   Removing a node in a binary search tree (degree 0)

    As shown in Figure 7-20, when the degree of the node to be removed is \\(1\\), replacing the node to be removed with its child node is sufficient.

    Figure 7-20   Removing a node in a binary search tree (degree 1)

    When the degree of the node to be removed is \\(2\\), we cannot directly remove it; instead, we need to use a node to replace it. To maintain the binary search tree's property of \"left subtree \\(<\\) root node \\(<\\) right subtree,\" this node can be either the smallest node in the right subtree or the largest node in the left subtree.

    Assuming we choose the smallest node in the right subtree, that is, the inorder successor, the removal process is as shown in Figure 7-21.

    1. Find the next node of the node to be removed in the \"inorder traversal sequence,\" denoted as tmp.
    2. Replace the value of the node to be removed with the value of tmp, and recursively remove node tmp in the tree.
    <1><2><3><4>

    Figure 7-21   Removing a node in a binary search tree (degree 2)

    The node removal operation also uses \\(O(\\log n)\\) time, where finding the node to be removed requires \\(O(\\log n)\\) time, and obtaining the inorder successor node requires \\(O(\\log n)\\) time. Example code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
    def remove(self, num: int):\n    \"\"\"Delete node\"\"\"\n    # If tree is empty, return directly\n    if self._root is None:\n        return\n    # Loop search, exit after passing leaf node\n    cur, pre = self._root, None\n    while cur is not None:\n        # Found node to delete, exit loop\n        if cur.val == num:\n            break\n        pre = cur\n        # Node to delete is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Node to delete is in cur's left subtree\n        else:\n            cur = cur.left\n    # If no node to delete, return directly\n    if cur is None:\n        return\n\n    # Number of child nodes = 0 or 1\n    if cur.left is None or cur.right is None:\n        # When number of child nodes = 0 / 1, child = null / that child node\n        child = cur.left or cur.right\n        # Delete node cur\n        if cur != self._root:\n            if pre.left == cur:\n                pre.left = child\n            else:\n                pre.right = child\n        else:\n            # If deleted node is root node, reassign root node\n            self._root = child\n    # Number of child nodes = 2\n    else:\n        # Get next node of cur in inorder traversal\n        tmp: TreeNode = cur.right\n        while tmp.left is not None:\n            tmp = tmp.left\n        # Recursively delete node tmp\n        self.remove(tmp.val)\n        # Replace cur with tmp\n        cur.val = tmp.val\n
    binary_search_tree.cpp
    /* Remove node */\nvoid remove(int num) {\n    // If tree is empty, return directly\n    if (root == nullptr)\n        return;\n    TreeNode *cur = root, *pre = nullptr;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Found node to delete, exit loop\n        if (cur->val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur->left;\n    }\n    // If no node to delete, return directly\n    if (cur == nullptr)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur->left == nullptr || cur->right == nullptr) {\n        // When number of child nodes = 0 / 1, child = nullptr / that child node\n        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre->left == cur)\n                pre->left = child;\n            else\n                pre->right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n        // Free memory\n        delete cur;\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode *tmp = cur->right;\n        while (tmp->left != nullptr) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Recursively delete node tmp\n        remove(tmp->val);\n        // Replace cur with tmp\n        cur->val = tmpVal;\n    }\n}\n
    binary_search_tree.java
    /* Remove node */\nvoid remove(int num) {\n    // If tree is empty, return directly\n    if (root == null)\n        return;\n    TreeNode cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        TreeNode child = cur.left != null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        remove(tmp.val);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
    binary_search_tree.cs
    /* Remove node */\nvoid Remove(int num) {\n    // If tree is empty, return directly\n    if (root == null)\n        return;\n    TreeNode? cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        TreeNode? child = cur.left ?? cur.right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre!.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode? tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        Remove(tmp.val!.Value);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
    binary_search_tree.go
    /* Remove node */\nfunc (bst *binarySearchTree) remove(num int) {\n    cur := bst.root\n    // If tree is empty, return directly\n    if cur == nil {\n        return\n    }\n    // Node position before the node to be removed\n    var pre *TreeNode = nil\n    // Loop search, exit after passing leaf node\n    for cur != nil {\n        if cur.Val == num {\n            break\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            // Node to be removed is in right subtree\n            cur = cur.Right\n        } else {\n            // Node to be removed is in left subtree\n            cur = cur.Left\n        }\n    }\n    // If no node to delete, return directly\n    if cur == nil {\n        return\n    }\n    // Number of child nodes is 0 or 1\n    if cur.Left == nil || cur.Right == nil {\n        var child *TreeNode = nil\n        // Get child node of node to be removed\n        if cur.Left != nil {\n            child = cur.Left\n        } else {\n            child = cur.Right\n        }\n        // Delete node 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            // If deleted node is root node, reassign root node\n            bst.root = child\n        }\n        // Number of child nodes is 2\n    } else {\n        // Get next node of node cur to be removed in in-order traversal\n        tmp := cur.Right\n        for tmp.Left != nil {\n            tmp = tmp.Left\n        }\n        // Recursively delete node tmp\n        bst.remove(tmp.Val.(int))\n        // Replace cur with tmp\n        cur.Val = tmp.Val\n    }\n}\n
    binary_search_tree.swift
    /* Remove node */\nfunc remove(num: Int) {\n    // If tree is empty, return directly\n    if root == nil {\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Found node to delete, exit loop\n        if cur!.val == num {\n            break\n        }\n        pre = cur\n        // Node to delete is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Node to delete is in cur's left subtree\n        else {\n            cur = cur?.left\n        }\n    }\n    // If no node to delete, return directly\n    if cur == nil {\n        return\n    }\n    // Number of child nodes = 0 or 1\n    if cur?.left == nil || cur?.right == nil {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        let child = cur?.left ?? cur?.right\n        // Delete node cur\n        if cur !== root {\n            if pre?.left === cur {\n                pre?.left = child\n            } else {\n                pre?.right = child\n            }\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        var tmp = cur?.right\n        while tmp?.left != nil {\n            tmp = tmp?.left\n        }\n        // Recursively delete node tmp\n        remove(num: tmp!.val)\n        // Replace cur with tmp\n        cur?.val = tmp!.val\n    }\n}\n
    binary_search_tree.js
    /* Remove node */\nremove(num) {\n    // If tree is empty, return directly\n    if (this.root === null) return;\n    let cur = this.root,\n        pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found node to delete, exit loop\n        if (cur.val === num) break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur === null) return;\n    // Number of child nodes = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        const child = cur.left !== null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur !== this.root) {\n            if (pre.left === cur) pre.left = child;\n            else pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            this.root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        let tmp = cur.right;\n        while (tmp.left !== null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        this.remove(tmp.val);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
    binary_search_tree.ts
    /* Remove node */\nremove(num: number): void {\n    // If tree is empty, return directly\n    if (this.root === null) return;\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found node to delete, exit loop\n        if (cur.val === num) break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur === null) return;\n    // Number of child nodes = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        const child: TreeNode | null =\n            cur.left !== null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur !== this.root) {\n            if (pre!.left === cur) pre!.left = child;\n            else pre!.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            this.root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        let tmp: TreeNode | null = cur.right;\n        while (tmp!.left !== null) {\n            tmp = tmp!.left;\n        }\n        // Recursively delete node tmp\n        this.remove(tmp!.val);\n        // Replace cur with tmp\n        cur.val = tmp!.val;\n    }\n}\n
    binary_search_tree.dart
    /* Remove node */\nvoid remove(int _num) {\n  // If tree is empty, return directly\n  if (_root == null) return;\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Found node to delete, exit loop\n    if (cur.val == _num) break;\n    pre = cur;\n    // Node to delete is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Node to delete is in cur's left subtree\n    else\n      cur = cur.left;\n  }\n  // If no node to delete, return directly\n  if (cur == null) return;\n  // Number of child nodes = 0 or 1\n  if (cur.left == null || cur.right == null) {\n    // When number of child nodes = 0 / 1, child = null / that child node\n    TreeNode? child = cur.left ?? cur.right;\n    // Delete node cur\n    if (cur != _root) {\n      if (pre!.left == cur)\n        pre.left = child;\n      else\n        pre.right = child;\n    } else {\n      // If deleted node is root node, reassign root node\n      _root = child;\n    }\n  } else {\n    // Number of child nodes = 2\n    // Get next node of cur in inorder traversal\n    TreeNode? tmp = cur.right;\n    while (tmp!.left != null) {\n      tmp = tmp.left;\n    }\n    // Recursively delete node tmp\n    remove(tmp.val);\n    // Replace cur with tmp\n    cur.val = tmp.val;\n  }\n}\n
    binary_search_tree.rs
    /* Remove node */\npub fn remove(&mut self, num: i32) {\n    // If tree is empty, return directly\n    if self.root.is_none() {\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Found node to delete, exit loop\n            Ordering::Equal => break,\n            // Node to delete is in cur's right subtree\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Node to delete is in cur's left subtree\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // If no node to delete, return directly\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        // Number of child nodes = 0 or 1\n        (None, None) | (Some(_), None) | (None, Some(_)) => {\n            // When number of child nodes = 0 / 1, child = nullptr / that child node\n            let child = left_child.or(right_child);\n            let pre = pre.unwrap();\n            // Delete node 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                // If deleted node is root node, reassign root node\n                self.root = child;\n            }\n        }\n        // Number of child nodes = 2\n        (Some(_), Some(_)) => {\n            // Get next node of cur in inorder traversal\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            // Recursively delete node tmp\n            self.remove(tmp_val);\n            // Replace cur with tmp\n            cur.borrow_mut().val = tmp_val;\n        }\n    }\n}\n
    binary_search_tree.c
    /* Remove node */\n// Cannot use remove keyword here due to stdio.h inclusion\nvoid removeItem(BinarySearchTree *bst, int num) {\n    // If tree is empty, return directly\n    if (bst->root == NULL)\n        return;\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        // Found node to delete, exit loop\n        if (cur->val == num)\n            break;\n        pre = cur;\n        if (cur->val < num) {\n            // Node to delete is in right subtree of root\n            cur = cur->right;\n        } else {\n            // Node to delete is in left subtree of root\n            cur = cur->left;\n        }\n    }\n    // If no node to delete, return directly\n    if (cur == NULL)\n        return;\n    // Check if node to delete has children\n    if (cur->left == NULL || cur->right == NULL) {\n        /* Number of child nodes = 0 or 1 */\n        // When number of child nodes = 0 / 1, child = nullptr / that child node\n        TreeNode *child = cur->left != NULL ? cur->left : cur->right;\n        // Delete node cur\n        if (pre->left == cur) {\n            pre->left = child;\n        } else {\n            pre->right = child;\n        }\n        // Free memory\n        free(cur);\n    } else {\n        /* Number of child nodes = 2 */\n        // Get next node of cur in inorder traversal\n        TreeNode *tmp = cur->right;\n        while (tmp->left != NULL) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Recursively delete node tmp\n        removeItem(bst, tmp->val);\n        // Replace cur with tmp\n        cur->val = tmpVal;\n    }\n}\n
    binary_search_tree.kt
    /* Remove node */\nfun remove(num: Int) {\n    // If tree is empty, return directly\n    if (root == null)\n        return\n    var cur = root\n    var pre: TreeNode? = null\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur._val == num)\n            break\n        pre = cur\n        // Node to delete is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Node to delete is in cur's left subtree\n        else\n            cur.left\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        val child = if (cur.left != null)\n            cur.left\n        else\n            cur.right\n        // Delete node cur\n        if (cur != root) {\n            if (pre!!.left == cur)\n                pre.left = child\n            else\n                pre.right = child\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child\n        }\n        // Number of child nodes = 2\n    } else {\n        // Get next node of cur in inorder traversal\n        var tmp = cur.right\n        while (tmp!!.left != null) {\n            tmp = tmp.left\n        }\n        // Recursively delete node tmp\n        remove(tmp._val)\n        // Replace cur with tmp\n        cur._val = tmp._val\n    }\n}\n
    binary_search_tree.rb
    ### Delete node ###\ndef remove(num)\n  # If tree is empty, return directly\n  return if @root.nil?\n\n  # Loop search, exit after passing leaf node\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Found node to delete, exit loop\n    break if cur.val == num\n\n    pre = cur\n    # Node to delete is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Node to delete is in cur's left subtree\n    else\n      cur = cur.left\n    end\n  end\n  # If no node to delete, return directly\n  return if cur.nil?\n\n  # Number of child nodes = 0 or 1\n  if cur.left.nil? || cur.right.nil?\n    # When number of child nodes = 0 / 1, child = null / that child node\n    child = cur.left || cur.right\n    # Delete node 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      # If deleted node is root node, reassign root node\n      @root = child\n    end\n  # Number of child nodes = 2\n  else\n    # Get next node of cur in inorder traversal\n    tmp = cur.right\n    while !tmp.left.nil?\n      tmp = tmp.left\n    end\n    # Recursively delete node tmp\n    remove(tmp.val)\n    # Replace cur with tmp\n    cur.val = tmp.val\n  end\nend\n
    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4-inorder-traversal-is-ordered","level":3,"title":"4.   Inorder Traversal Is Ordered","text":"

    As shown in Figure 7-22, the inorder traversal of a binary tree follows the \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\" traversal order, while the binary search tree satisfies the \"left child node \\(<\\) root node \\(<\\) right child node\" size relationship.

    This means that when performing an inorder traversal in a binary search tree, the next smallest node is always traversed first, thus yielding an important property: The inorder traversal sequence of a binary search tree is ascending.

    Using the property of inorder traversal being ascending, we can obtain ordered data in a binary search tree in only \\(O(n)\\) time, without the need for additional sorting operations, which is very efficient.

    Figure 7-22   Inorder traversal sequence of a binary search tree

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742-efficiency-of-binary-search-trees","level":2,"title":"7.4.2   Efficiency of Binary Search Trees","text":"

    Given a set of data, we consider using an array or a binary search tree for storage. Observing Table 7-2, all operations in a binary search tree have logarithmic time complexity, providing stable and efficient performance. Arrays are more efficient than binary search trees only in scenarios with high-frequency additions and low-frequency searches and deletions.

    Table 7-2   Efficiency comparison between arrays and search trees

    Unsorted array Binary search tree Search element \\(O(n)\\) \\(O(\\log n)\\) Insert element \\(O(1)\\) \\(O(\\log n)\\) Remove element \\(O(n)\\) \\(O(\\log n)\\)

    In the ideal case, a binary search tree is balanced, so any node can be found within \\(O(\\log n)\\) loop iterations.

    However, if we continuously insert and remove nodes in a binary search tree, it may degenerate into a linked list as shown in Figure 7-23, where the time complexity of various operations also degrades to \\(O(n)\\).

    Figure 7-23   Degradation of a binary search tree

    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743-common-applications-of-binary-search-trees","level":2,"title":"7.4.3   Common Applications of Binary Search Trees","text":"
    • Used as multi-level indexes in systems to implement efficient search, insertion, and removal operations.
    • Serves as the underlying data structure for certain search algorithms.
    • Used to store data streams to maintain their ordered state.
    ","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   Binary Tree","text":"

    A binary tree is a non-linear data structure that models the hierarchical relationship between \"ancestors\" and \"descendants\" and embodies a divide-and-conquer pattern in which each split branches into two. Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    class TreeNode:\n    \"\"\"Binary tree node\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Node value\n        self.left: TreeNode | None = None  # Reference to left child node\n        self.right: TreeNode | None = None # Reference to right child node\n
    /* Binary tree node */\nstruct TreeNode {\n    int val;          // Node value\n    TreeNode *left;   // Pointer to left child node\n    TreeNode *right;  // Pointer to right child node\n    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n
    /* Binary tree node */\nclass TreeNode {\n    int val;         // Node value\n    TreeNode left;   // Reference to left child node\n    TreeNode right;  // Reference to right child node\n    TreeNode(int x) { val = x; }\n}\n
    /* Binary tree node */\nclass TreeNode(int? x) {\n    public int? val = x;    // Node value\n    public TreeNode? left;  // Reference to left child node\n    public TreeNode? right; // Reference to right child node\n}\n
    /* Binary tree node */\ntype TreeNode struct {\n    Val   int\n    Left  *TreeNode\n    Right *TreeNode\n}\n/* Constructor */\nfunc NewTreeNode(v int) *TreeNode {\n    return &TreeNode{\n        Left:  nil, // Pointer to left child node\n        Right: nil, // Pointer to right child node\n        Val:   v,   // Node value\n    }\n}\n
    /* Binary tree node */\nclass TreeNode {\n    var val: Int // Node value\n    var left: TreeNode? // Reference to left child node\n    var right: TreeNode? // Reference to right child node\n\n    init(x: Int) {\n        val = x\n    }\n}\n
    /* Binary tree node */\nclass TreeNode {\n    val; // Node value\n    left; // Pointer to left child node\n    right; // Pointer to right child node\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
    /* Binary tree node */\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; // Node value\n        this.left = left === undefined ? null : left; // Reference to left child node\n        this.right = right === undefined ? null : right; // Reference to right child node\n    }\n}\n
    /* Binary tree node */\nclass TreeNode {\n  int val;         // Node value\n  TreeNode? left;  // Reference to left child node\n  TreeNode? right; // Reference to right child node\n  TreeNode(this.val, [this.left, this.right]);\n}\n
    use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Binary tree node */\nstruct TreeNode {\n    val: i32,                               // Node value\n    left: Option<Rc<RefCell<TreeNode>>>,    // Reference to left child node\n    right: Option<Rc<RefCell<TreeNode>>>,   // Reference to right child node\n}\n\nimpl TreeNode {\n    /* Constructor */\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
    /* Binary tree node */\ntypedef struct TreeNode {\n    int val;                // Node value\n    int height;             // Node height\n    struct TreeNode *left;  // Pointer to left child node\n    struct TreeNode *right; // Pointer to right child node\n} TreeNode;\n\n/* Constructor */\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
    /* Binary tree node */\nclass TreeNode(val _val: Int) {  // Node value\n    val left: TreeNode? = null   // Reference to left child node\n    val right: TreeNode? = null  // Reference to right child node\n}\n
    ### Binary tree node class ###\nclass TreeNode\n  attr_accessor :val    # Node value\n  attr_accessor :left   # Reference to left child node\n  attr_accessor :right  # Reference to right child node\n\n  def initialize(val)\n    @val = val\n  end\nend\n

    Each node has two references (pointers), pointing respectively to the left-child node and right-child node. This node is called the parent node of these two child nodes. When given a node of a binary tree, we call the tree formed by this node's left child and all nodes below it the left subtree of this node. Similarly, the right subtree can be defined.

    In a binary tree, every non-leaf node has child nodes and therefore non-empty subtrees. As shown in Figure 7-1, if \"Node 2\" is regarded as a parent node, its left and right child nodes are \"Node 4\" and \"Node 5\" respectively. The left subtree is formed by \"Node 4\" and all nodes beneath it, while the right subtree is formed by \"Node 5\" and all nodes beneath it.

    Figure 7-1   Parent Node, child Node, subtree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#711-common-terminology-of-binary-trees","level":2,"title":"7.1.1   Common Terminology of Binary Trees","text":"

    The commonly used terminology of binary trees is shown in Figure 7-2.

    • Root node: The node at the top level of a binary tree, which does not have a parent node.
    • Leaf node: A node that does not have any child nodes, with both of its pointers pointing to None.
    • Edge: A line segment that connects two nodes, representing a reference (pointer) between the nodes.
    • The level of a node: It increases from top to bottom, with the root node being at level 1.
    • The degree of a node: The number of child nodes that a node has. In a binary tree, the degree can be 0, 1, or 2.
    • The height of a binary tree: The number of edges from the root node to the farthest leaf node.
    • The depth of a node: The number of edges from the root node to the node.
    • The height of a node: The number of edges from the farthest leaf node to the node.

    Figure 7-2   Common Terminology of Binary Trees

    Tip

    We usually define \"height\" and \"depth\" as the number of edges traversed, but some textbooks and problem statements define them as the number of nodes on the path. In that case, both values are larger by 1.

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#712-basic-operations-of-binary-trees","level":2,"title":"7.1.2   Basic Operations of Binary Trees","text":"","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#1-initializing-a-binary-tree","level":3,"title":"1.   Initializing a Binary Tree","text":"

    Similar to a linked list, the initialization of a binary tree involves first creating the nodes and then establishing the references (pointers) between them.

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
    # Initializing a binary tree\n# Initializing nodes\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
    binary_tree.cpp
    /* Initializing a binary tree */\n// Initializing nodes\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// Linking references (pointers) between nodes\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
    binary_tree.java
    // Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.cs
    /* Initializing a binary tree */\n// Initializing nodes\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.go
    /* Initializing a binary tree */\n// Initializing nodes\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// Linking references (pointers) between nodes\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n
    binary_tree.swift
    // Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
    binary_tree.js
    /* Initializing a binary tree */\n// Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.ts
    /* Initializing a binary tree */\n// Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.dart
    /* Initializing a binary tree */\n// Initializing nodes\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// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
    binary_tree.rs
    // Initializing nodes\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// Linking references (pointers) between nodes\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
    /* Initializing a binary tree */\n// Initializing nodes\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// Linking references (pointers) between nodes\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
    binary_tree.kt
    // Initializing nodes\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
    binary_tree.rb
    # Initializing a binary tree\n# Initializing nodes\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
    Code Visualization

    Full Screen >

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#2-inserting-and-removing-nodes","level":3,"title":"2.   Inserting and Removing Nodes","text":"

    Similar to a linked list, inserting and removing nodes in a binary tree can be achieved by modifying pointers. Figure 7-3 provides an example.

    Figure 7-3   Inserting and removing nodes in a binary tree

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
    # Inserting and removing nodes\np = TreeNode(0)\n# Inserting node P between n1 -> n2\nn1.left = p\np.left = n2\n# Removing node P\nn1.left = n2\n
    binary_tree.cpp
    /* Inserting and removing nodes */\nTreeNode* P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1->left = P;\nP->left = n2;\n// Removing node P\nn1->left = n2;\n
    binary_tree.java
    TreeNode P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.cs
    /* Inserting and removing nodes */\nTreeNode P = new(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.go
    /* Inserting and removing nodes */\n// Inserting node P between n1 and n2\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// Removing node P\nn1.Left = n2\n
    binary_tree.swift
    let P = TreeNode(x: 0)\n// Inserting node P between n1 and n2\nn1.left = P\nP.left = n2\n// Removing node P\nn1.left = n2\n
    binary_tree.js
    /* Inserting and removing nodes */\nlet P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.ts
    /* Inserting and removing nodes */\nconst P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.dart
    /* Inserting and removing nodes */\nTreeNode P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
    binary_tree.rs
    let p = TreeNode::new(0);\n// Inserting node P between n1 and n2\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// Removing node P\nn1.borrow_mut().left = Some(n2);\n
    binary_tree.c
    /* Inserting and removing nodes */\nTreeNode *P = newTreeNode(0);\n// Inserting node P between n1 and n2\nn1->left = P;\nP->left = n2;\n// Removing node P\nn1->left = n2;\n
    binary_tree.kt
    val P = TreeNode(0)\n// Inserting node P between n1 and n2\nn1.left = P\nP.left = n2\n// Removing node P\nn1.left = n2\n
    binary_tree.rb
    # Inserting and removing nodes\n_p = TreeNode.new(0)\n# Inserting node _p between n1 and n2\nn1.left = _p\n_p.left = n2\n# Removing node _p\nn1.left = n2\n
    Code Visualization

    Full Screen >

    Tip

    Keep in mind that inserting a node can alter the original logical structure of a binary tree, while deleting a node usually entails removing that node together with its entire subtree. In practice, insertion and deletion in binary trees are therefore typically implemented as coordinated sequences of operations to achieve a meaningful result.

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#713-common-types-of-binary-trees","level":2,"title":"7.1.3   Common Types of Binary Trees","text":"","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#1-perfect-binary-tree","level":3,"title":"1.   Perfect Binary Tree","text":"

    As shown in Figure 7-4, a perfect binary tree has every level completely filled. In a perfect binary tree, leaf nodes have a degree of \\(0\\), while all other nodes have a degree of \\(2\\). If the tree height is \\(h\\), the total number of nodes is \\(2^{h+1} - 1\\), following a standard exponential pattern that mirrors the common phenomenon of cell division in nature.

    Tip

    Please note that in the Chinese community, a perfect binary tree is often referred to as a full binary tree.

    Figure 7-4   Perfect binary tree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#2-complete-binary-tree","level":3,"title":"2.   Complete Binary Tree","text":"

    As shown in Figure 7-5, a complete binary tree only allows the bottom level to be incompletely filled, and the nodes at the bottom level must be filled continuously from left to right. Note that a perfect binary tree is also a complete binary tree.

    Figure 7-5   Complete binary tree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#3-full-binary-tree","level":3,"title":"3.   Full Binary Tree","text":"

    As shown in Figure 7-6, in a full binary tree, all nodes except leaf nodes have two child nodes.

    Figure 7-6   Full binary tree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#4-balanced-binary-tree","level":3,"title":"4.   Balanced Binary Tree","text":"

    As shown in Figure 7-7, in a balanced binary tree, the absolute difference between the height of the left and right subtrees of any node does not exceed 1.

    Figure 7-7   Balanced binary tree

    ","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#714-degeneration-of-binary-trees","level":2,"title":"7.1.4   Degeneration of Binary Trees","text":"

    Figure 7-8 contrasts the ideal and degenerate structures of binary trees. When every level is filled, the tree becomes a \"perfect binary tree\"; when all nodes skew to one side, the binary tree degenerates into a \"linked list\".

    • A perfect binary tree is the ideal case, fully leveraging the divide-and-conquer advantages of binary trees.
    • A linked list represents the other extreme, where all operations become linear operations with time complexity degrading to \\(O(n)\\).

    Figure 7-8   The Best and Worst Structures of Binary Trees

    As shown in Table 7-1, in the best and worst structures, the binary tree achieves either maximum or minimum values for leaf node count, total number of nodes, and height.

    Table 7-1   The Best and Worst Structures of Binary Trees

    Perfect binary tree Linked list Number of nodes at level \\(i\\) \\(2^{i-1}\\) \\(1\\) Number of leaf nodes in a tree with height \\(h\\) \\(2^h\\) \\(1\\) Total number of nodes in a tree with height \\(h\\) \\(2^{h+1} - 1\\) \\(h + 1\\) Height of a tree with \\(n\\) total nodes \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   Binary Tree Traversal","text":"

    From a physical structure perspective, a tree is a data structure based on linked lists. Hence, its traversal method involves accessing nodes one by one through pointers. However, a tree is a non-linear data structure, which makes traversing a tree more complex than traversing a linked list, requiring the assistance of search algorithms.

    The common traversal methods for binary trees include level-order traversal, pre-order traversal, in-order traversal, and post-order traversal.

    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721-level-order-traversal","level":2,"title":"7.2.1   Level-Order Traversal","text":"

    As shown in Figure 7-9, level-order traversal traverses the binary tree from top to bottom, layer by layer. Within each level, it visits nodes from left to right.

    Level-order traversal is essentially breadth-first traversal, also known as breadth-first search (BFS), which proceeds outward level by level.

    Figure 7-9   Level-order traversal of a binary tree

    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1-code-implementation","level":3,"title":"1.   Code Implementation","text":"

    Breadth-first traversal is typically implemented with the help of a \"queue\". The queue follows the \"first in, first out\" rule, while breadth-first traversal follows the \"layer-by-layer progression\" rule; the underlying ideas of the two are consistent. The implementation code is as follows:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py
    def level_order(root: TreeNode | None) -> list[int]:\n    \"\"\"Level-order traversal\"\"\"\n    # Initialize queue, add root node\n    queue: deque[TreeNode] = deque()\n    queue.append(root)\n    # Initialize a list to save the traversal sequence\n    res = []\n    while queue:\n        node: TreeNode = queue.popleft()  # Dequeue\n        res.append(node.val)  # Save node value\n        if node.left is not None:\n            queue.append(node.left)  # Left child node enqueue\n        if node.right is not None:\n            queue.append(node.right)  # Right child node enqueue\n    return res\n
    binary_tree_bfs.cpp
    /* Level-order traversal */\nvector<int> levelOrder(TreeNode *root) {\n    // Initialize queue, add root node\n    queue<TreeNode *> queue;\n    queue.push(root);\n    // Initialize a list to save the traversal sequence\n    vector<int> vec;\n    while (!queue.empty()) {\n        TreeNode *node = queue.front();\n        queue.pop();              // Dequeue\n        vec.push_back(node->val); // Save node value\n        if (node->left != nullptr)\n            queue.push(node->left); // Left child node enqueue\n        if (node->right != nullptr)\n            queue.push(node->right); // Right child node enqueue\n    }\n    return vec;\n}\n
    binary_tree_bfs.java
    /* Level-order traversal */\nList<Integer> levelOrder(TreeNode root) {\n    // Initialize queue, add root node\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root);\n    // Initialize a list to save the traversal sequence\n    List<Integer> list = new ArrayList<>();\n    while (!queue.isEmpty()) {\n        TreeNode node = queue.poll(); // Dequeue\n        list.add(node.val);           // Save node value\n        if (node.left != null)\n            queue.offer(node.left);   // Left child node enqueue\n        if (node.right != null)\n            queue.offer(node.right);  // Right child node enqueue\n    }\n    return list;\n}\n
    binary_tree_bfs.cs
    /* Level-order traversal */\nList<int> LevelOrder(TreeNode root) {\n    // Initialize queue, add root node\n    Queue<TreeNode> queue = new();\n    queue.Enqueue(root);\n    // Initialize a list to save the traversal sequence\n    List<int> list = [];\n    while (queue.Count != 0) {\n        TreeNode node = queue.Dequeue(); // Dequeue\n        list.Add(node.val!.Value);       // Save node value\n        if (node.left != null)\n            queue.Enqueue(node.left);    // Left child node enqueue\n        if (node.right != null)\n            queue.Enqueue(node.right);   // Right child node enqueue\n    }\n    return list;\n}\n
    binary_tree_bfs.go
    /* Level-order traversal */\nfunc levelOrder(root *TreeNode) []any {\n    // Initialize queue, add root node\n    queue := list.New()\n    queue.PushBack(root)\n    // Initialize a slice to save traversal sequence\n    nums := make([]any, 0)\n    for queue.Len() > 0 {\n        // Dequeue\n        node := queue.Remove(queue.Front()).(*TreeNode)\n        // Save node value\n        nums = append(nums, node.Val)\n        if node.Left != nil {\n            // Left child node enqueue\n            queue.PushBack(node.Left)\n        }\n        if node.Right != nil {\n            // Right child node enqueue\n            queue.PushBack(node.Right)\n        }\n    }\n    return nums\n}\n
    binary_tree_bfs.swift
    /* Level-order traversal */\nfunc levelOrder(root: TreeNode) -> [Int] {\n    // Initialize queue, add root node\n    var queue: [TreeNode] = [root]\n    // Initialize a list to save the traversal sequence\n    var list: [Int] = []\n    while !queue.isEmpty {\n        let node = queue.removeFirst() // Dequeue\n        list.append(node.val) // Save node value\n        if let left = node.left {\n            queue.append(left) // Left child node enqueue\n        }\n        if let right = node.right {\n            queue.append(right) // Right child node enqueue\n        }\n    }\n    return list\n}\n
    binary_tree_bfs.js
    /* Level-order traversal */\nfunction levelOrder(root) {\n    // Initialize queue, add root node\n    const queue = [root];\n    // Initialize a list to save the traversal sequence\n    const list = [];\n    while (queue.length) {\n        let node = queue.shift(); // Dequeue\n        list.push(node.val); // Save node value\n        if (node.left) queue.push(node.left); // Left child node enqueue\n        if (node.right) queue.push(node.right); // Right child node enqueue\n    }\n    return list;\n}\n
    binary_tree_bfs.ts
    /* Level-order traversal */\nfunction levelOrder(root: TreeNode | null): number[] {\n    // Initialize queue, add root node\n    const queue = [root];\n    // Initialize a list to save the traversal sequence\n    const list: number[] = [];\n    while (queue.length) {\n        let node = queue.shift() as TreeNode; // Dequeue\n        list.push(node.val); // Save node value\n        if (node.left) {\n            queue.push(node.left); // Left child node enqueue\n        }\n        if (node.right) {\n            queue.push(node.right); // Right child node enqueue\n        }\n    }\n    return list;\n}\n
    binary_tree_bfs.dart
    /* Level-order traversal */\nList<int> levelOrder(TreeNode? root) {\n  // Initialize queue, add root node\n  Queue<TreeNode?> queue = Queue();\n  queue.add(root);\n  // Initialize a list to save the traversal sequence\n  List<int> res = [];\n  while (queue.isNotEmpty) {\n    TreeNode? node = queue.removeFirst(); // Dequeue\n    res.add(node!.val); // Save node value\n    if (node.left != null) queue.add(node.left); // Left child node enqueue\n    if (node.right != null) queue.add(node.right); // Right child node enqueue\n  }\n  return res;\n}\n
    binary_tree_bfs.rs
    /* Level-order traversal */\nfn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {\n    // Initialize queue, add root node\n    let mut que = VecDeque::new();\n    que.push_back(root.clone());\n    // Initialize a list to save the traversal sequence\n    let mut vec = Vec::new();\n\n    while let Some(node) = que.pop_front() {\n        // Dequeue\n        vec.push(node.borrow().val); // Save node value\n        if let Some(left) = node.borrow().left.as_ref() {\n            que.push_back(left.clone()); // Left child node enqueue\n        }\n        if let Some(right) = node.borrow().right.as_ref() {\n            que.push_back(right.clone()); // Right child node enqueue\n        };\n    }\n    vec\n}\n
    binary_tree_bfs.c
    /* Level-order traversal */\nint *levelOrder(TreeNode *root, int *size) {\n    /* Auxiliary queue */\n    int front, rear;\n    int index, *arr;\n    TreeNode *node;\n    TreeNode **queue;\n\n    /* Auxiliary queue */\n    queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n    // Queue pointer\n    front = 0, rear = 0;\n    // Add root node\n    queue[rear++] = root;\n    // Initialize a list to save the traversal sequence\n    /* Auxiliary array */\n    arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n    // Array pointer\n    index = 0;\n    while (front < rear) {\n        // Dequeue\n        node = queue[front++];\n        // Save node value\n        arr[index++] = node->val;\n        if (node->left != NULL) {\n            // Left child node enqueue\n            queue[rear++] = node->left;\n        }\n        if (node->right != NULL) {\n            // Right child node enqueue\n            queue[rear++] = node->right;\n        }\n    }\n    // Update array length value\n    *size = index;\n    arr = realloc(arr, sizeof(int) * (*size));\n\n    // Free auxiliary array space\n    free(queue);\n    return arr;\n}\n
    binary_tree_bfs.kt
    /* Level-order traversal */\nfun levelOrder(root: TreeNode?): MutableList<Int> {\n    // Initialize queue, add root node\n    val queue = LinkedList<TreeNode?>()\n    queue.add(root)\n    // Initialize a list to save the traversal sequence\n    val list = mutableListOf<Int>()\n    while (queue.isNotEmpty()) {\n        val node = queue.poll()      // Dequeue\n        list.add(node?._val!!)       // Save node value\n        if (node.left != null)\n            queue.offer(node.left)   // Left child node enqueue\n        if (node.right != null)\n            queue.offer(node.right)  // Right child node enqueue\n    }\n    return list\n}\n
    binary_tree_bfs.rb
    ### Level-order traversal ###\ndef level_order(root)\n  # Initialize queue, add root node\n  queue = [root]\n  # Initialize a list to save the traversal sequence\n  res = []\n  while !queue.empty?\n    node = queue.shift # Dequeue\n    res << node.val # Save node value\n    queue << node.left unless node.left.nil? # Left child node enqueue\n    queue << node.right unless node.right.nil? # Right child node enqueue\n  end\n  res\nend\n
    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2-complexity-analysis","level":3,"title":"2.   Complexity Analysis","text":"
    • Time complexity is \\(O(n)\\): All nodes are visited once, using \\(O(n)\\) time, where \\(n\\) is the number of nodes.
    • Space complexity is \\(O(n)\\): In the worst case, i.e., a full binary tree, before traversing to the bottom level, the queue contains at most \\((n + 1) / 2\\) nodes simultaneously, occupying \\(O(n)\\) space.
    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722-preorder-inorder-and-postorder-traversal","level":2,"title":"7.2.2   Preorder, Inorder, and Postorder Traversal","text":"

    Correspondingly, preorder, inorder, and postorder traversals all belong to depth-first traversal, also known as depth-first search (DFS), which goes as deep as possible before backtracking.

    Figure 7-10 shows how depth-first traversal works on a binary tree. Depth-first traversal is like \"walking\" around the perimeter of the entire binary tree, encountering three positions at each node, corresponding to preorder, inorder, and postorder traversal.

    Figure 7-10   Preorder, inorder, and postorder traversal of a binary tree

    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1-code-implementation_1","level":3,"title":"1.   Code Implementation","text":"

    Depth-first search is usually implemented based on recursion:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py
    def pre_order(root: TreeNode | None):\n    \"\"\"Preorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: root node -> left subtree -> right subtree\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    \"\"\"Inorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: left subtree -> root node -> right subtree\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    \"\"\"Postorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: left subtree -> right subtree -> root node\n    post_order(root=root.left)\n    post_order(root=root.right)\n    res.append(root.val)\n
    binary_tree_dfs.cpp
    /* Preorder traversal */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    vec.push_back(root->val);\n    preOrder(root->left);\n    preOrder(root->right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root->left);\n    vec.push_back(root->val);\n    inOrder(root->right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root->left);\n    postOrder(root->right);\n    vec.push_back(root->val);\n}\n
    binary_tree_dfs.java
    /* Preorder traversal */\nvoid preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.add(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.add(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.add(root.val);\n}\n
    binary_tree_dfs.cs
    /* Preorder traversal */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.Add(root.val!.Value);\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n\n/* Inorder traversal */\nvoid InOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: left subtree -> root node -> right subtree\n    InOrder(root.left);\n    list.Add(root.val!.Value);\n    InOrder(root.right);\n}\n\n/* Postorder traversal */\nvoid PostOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: left subtree -> right subtree -> root node\n    PostOrder(root.left);\n    PostOrder(root.right);\n    list.Add(root.val!.Value);\n}\n
    binary_tree_dfs.go
    /* Preorder traversal */\nfunc preOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    nums = append(nums, node.Val)\n    preOrder(node.Left)\n    preOrder(node.Right)\n}\n\n/* Inorder traversal */\nfunc inOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(node.Left)\n    nums = append(nums, node.Val)\n    inOrder(node.Right)\n}\n\n/* Postorder traversal */\nfunc postOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(node.Left)\n    postOrder(node.Right)\n    nums = append(nums, node.Val)\n}\n
    binary_tree_dfs.swift
    /* Preorder traversal */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    list.append(root.val)\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n\n/* Inorder traversal */\nfunc inOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root: root.left)\n    list.append(root.val)\n    inOrder(root: root.right)\n}\n\n/* Postorder traversal */\nfunc postOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root: root.left)\n    postOrder(root: root.right)\n    list.append(root.val)\n}\n
    binary_tree_dfs.js
    /* Preorder traversal */\nfunction preOrder(root) {\n    if (root === null) return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nfunction inOrder(root) {\n    if (root === null) return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nfunction postOrder(root) {\n    if (root === null) return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
    binary_tree_dfs.ts
    /* Preorder traversal */\nfunction preOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nfunction inOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nfunction postOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
    binary_tree_dfs.dart
    /* Preorder traversal */\nvoid preOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: root node -> left subtree -> right subtree\n  list.add(node.val);\n  preOrder(node.left);\n  preOrder(node.right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: left subtree -> root node -> right subtree\n  inOrder(node.left);\n  list.add(node.val);\n  inOrder(node.right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: left subtree -> right subtree -> root node\n  postOrder(node.left);\n  postOrder(node.right);\n  list.add(node.val);\n}\n
    binary_tree_dfs.rs
    /* Preorder traversal */\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            // Visit priority: root node -> left subtree -> right subtree\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/* Inorder traversal */\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            // Visit priority: left subtree -> root node -> right subtree\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/* Postorder traversal */\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            // Visit priority: left subtree -> right subtree -> root node\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
    /* Preorder traversal */\nvoid preOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    arr[(*size)++] = root->val;\n    preOrder(root->left, size);\n    preOrder(root->right, size);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root->left, size);\n    arr[(*size)++] = root->val;\n    inOrder(root->right, size);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root->left, size);\n    postOrder(root->right, size);\n    arr[(*size)++] = root->val;\n}\n
    binary_tree_dfs.kt
    /* Preorder traversal */\nfun preOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: root node -> left subtree -> right subtree\n    list.add(root._val)\n    preOrder(root.left)\n    preOrder(root.right)\n}\n\n/* Inorder traversal */\nfun inOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left)\n    list.add(root._val)\n    inOrder(root.right)\n}\n\n/* Postorder traversal */\nfun postOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left)\n    postOrder(root.right)\n    list.add(root._val)\n}\n
    binary_tree_dfs.rb
    ### Pre-order traversal ###\ndef pre_order(root)\n  return if root.nil?\n\n  # Visit priority: root node -> left subtree -> right subtree\n  $res << root.val\n  pre_order(root.left)\n  pre_order(root.right)\nend\n\n### In-order traversal ###\ndef in_order(root)\n  return if root.nil?\n\n  # Visit priority: left subtree -> root node -> right subtree\n  in_order(root.left)\n  $res << root.val\n  in_order(root.right)\nend\n\n### Post-order traversal ###\ndef post_order(root)\n  return if root.nil?\n\n  # Visit priority: left subtree -> right subtree -> root node\n  post_order(root.left)\n  post_order(root.right)\n  $res << root.val\nend\n

    Tip

    Depth-first search can also be implemented iteratively, and interested readers can explore this on their own.

    Figure 7-11 shows the recursive process of preorder traversal of a binary tree, which can be divided into two opposite phases: \"descending\" and \"returning\".

    1. \"Descending\" means making a new recursive call, during which the program visits the next node.
    2. \"Returning\" means the function call returns, indicating that the current node has been fully processed.
    <1><2><3><4><5><6><7><8><9><10><11>

    Figure 7-11   The recursive process of preorder traversal

    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2-complexity-analysis_1","level":3,"title":"2.   Complexity Analysis","text":"
    • Time complexity is \\(O(n)\\): All nodes are visited once, using \\(O(n)\\) time.
    • Space complexity is \\(O(n)\\): In the worst case, i.e., the tree degenerates into a linked list, the recursion depth reaches \\(n\\), and the system occupies \\(O(n)\\) stack frame space.
    ","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   Summary","text":"","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]},{"location":"chapter_tree/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
    • A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of splitting into two. Each binary tree node contains a value and two pointers, which point to its left and right child nodes.
    • For a certain node in a binary tree, the tree formed by its left (right) child node and all nodes below is called the left (right) subtree of that node.
    • Related terminology of binary trees includes root node, leaf node, level, degree, edge, height, and depth.
    • The initialization, node insertion, and node removal operations of binary trees are similar to those of linked lists.
    • Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. A perfect binary tree is the ideal form, while a linked list represents the worst degenerate case.
    • A binary tree can be represented using an array by arranging node values and empty slots in level-order traversal sequence, and implementing pointers based on the index mapping relationship between parent and child nodes.
    • Level-order traversal of a binary tree is a breadth-first search method that proceeds level by level, typically implemented using a queue.
    • Preorder, inorder, and postorder traversals all belong to depth-first search, which proceeds by going as deep as possible before backtracking, typically using recursion.
    • A binary search tree is an efficient data structure for element searching, with search, insertion, and removal operations all having time complexity of \\(O(\\log n)\\). When a binary search tree degenerates into a linked list, all time complexities degrade to \\(O(n)\\).
    • An AVL tree, also known as a balanced binary search tree, ensures the tree remains balanced after continuous node insertions and removals through rotation operations.
    • Rotation operations in AVL trees include right rotation, left rotation, right rotation followed by left rotation, and left rotation followed by right rotation. After inserting or removing nodes, AVL trees perform rotations from bottom to top to restore balance.
    ","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q: For a binary tree with only one node, are both the height of the tree and the depth of the root node \\(0\\)?

    Yes, because height and depth are typically defined as the number of edges on the path.

    Q: The insertion and removal in a binary tree are generally accomplished by a set of operations. What does \"a set of operations\" refer to here? Does it imply releasing the resources of the child nodes?

    Taking the binary search tree as an example, the operation of removing a node needs to be handled in three different scenarios, each requiring multiple steps of node operations.

    Q: Why does DFS traversal of binary trees have three orders: preorder, inorder, and postorder, and what are their uses?

    Similar to forward and reverse traversal of arrays, preorder, inorder, and postorder traversals are three methods of binary tree traversal that allow us to obtain a traversal result in a specific order. For example, in a binary search tree, since nodes satisfy the relationship left child node value < root node value < right child node value, we only need to traverse the tree with the priority of \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\" to obtain an ordered node sequence.

    Q: In a right rotation operation handling the relationship between unbalanced nodes node, child, and grand_child, doesn't the connection between node and its parent node get lost after the right rotation?

    We need to view this problem from a recursive perspective. The right rotation operation right_rotate(root) passes in the root node of the subtree and eventually returns the root node of the subtree after rotation with return child. The connection between the subtree's root node and its parent node is completed after the function returns, which is not within the maintenance scope of the right rotation operation.

    Q: In C++, functions are divided into private and public sections. What considerations are there for this? Why are the height() function and the updateHeight() function placed in public and private, respectively?

    It mainly depends on the method's usage scope. If a method is only used within the class, then it is designed as private. For example, calling updateHeight() alone by the user makes no sense, as it is only a step in insertion or removal operations. However, height() is used to access node height, similar to vector.size(), so it is set to public for ease of use.

    Q: How do you build a binary search tree from a set of input data? Is the choice of root node very important?

    Yes, the method for building a tree is provided in the build_tree() method in the binary search tree code. As for the choice of root node, we typically sort the input data, then select the middle element as the root node, and recursively build the left and right subtrees. This approach maximizes the tree's balance.

    Q: In Java, do you always have to use the equals() method for string comparison?

    In Java, for primitive data types, == is used to compare whether the values of two variables are equal. For reference types, the working principles of the two symbols are different.

    • ==: Used to compare whether two variables point to the same object, i.e., whether their positions in memory are the same.
    • equals(): Used to compare whether the values of two objects are equal.

    Therefore, if we want to compare values, we should use equals(). However, strings initialized via String a = \"hi\"; String b = \"hi\"; are stored in the string constant pool and point to the same object, so a == b can also be used to compare the contents of the two strings.

    Q: Before reaching the bottom level, is the number of nodes in the queue \\(2^h\\) in breadth-first traversal?

    Yes, for example, a full binary tree with height \\(h = 2\\) has a total of \\(n = 7\\) nodes, then the bottom level has \\(4 = 2^h = (n + 1) / 2\\) nodes.

    ","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]}]} \ No newline at end of file diff --git a/en/stylesheets/animation_player.css b/en/stylesheets/animation_player.css index 46c2b9da8..d7d23a507 100644 --- a/en/stylesheets/animation_player.css +++ b/en/stylesheets/animation_player.css @@ -176,4 +176,4 @@ font-size: 0.7rem; } } -/*! update cache: 20260410024939 */ +/*! update cache: 20260410223942 */ diff --git a/en/stylesheets/extra.css b/en/stylesheets/extra.css index 2b6340ff6..3e63db9f7 100644 --- a/en/stylesheets/extra.css +++ b/en/stylesheets/extra.css @@ -790,4 +790,4 @@ a:hover .device-on-hover { flex: 1 1 30%; } } -/*! update cache: 20260410024939 */ +/*! update cache: 20260410223942 */ diff --git a/en/stylesheets/giscus-dark.css b/en/stylesheets/giscus-dark.css index 69b7a8ef4..6a0d242ba 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: 20260410024939 */ +/*! update cache: 20260410223942 */ diff --git a/en/stylesheets/giscus-light.css b/en/stylesheets/giscus-light.css index 1ae87f44c..2b8a200eb 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: 20260410024939 */ +/*! update cache: 20260410223942 */ diff --git a/ja/assets/javascripts/bundle.c2b142ea.min.js b/ja/assets/javascripts/bundle.c2b142ea.min.js index ef612ff1d..02db34934 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: 20260410024950 */ +/*! update cache: 20260410223953 */ diff --git a/ja/chapter_data_structure/character_encoding/index.html b/ja/chapter_data_structure/character_encoding/index.html index b670e2bae..5ed2a7adb 100644 --- a/ja/chapter_data_structure/character_encoding/index.html +++ b/ja/chapter_data_structure/character_encoding/index.html @@ -4437,9 +4437,8 @@

    3.4.3   Unicode 文字セット

    コンピュータ技術が急速に発展するにつれて、文字セットと符号化規格は百花繚乱の状態となり、それに伴って多くの問題も生じました。一方では、これらの文字セットは通常、特定の言語の文字しか定義しておらず、多言語環境では正常に動作できませんでした。もう一方では、同じ言語にも複数の文字セット規格が存在し、2 台のコンピュータが異なる符号化規格を使っていると、情報伝達の際に文字化けが発生しました。

    当時の研究者たちはこう考えました。十分に完全な文字セットを打ち出して、世界中のあらゆる言語と記号をそこに収録すれば、多言語環境や文字化けの問題を解決できるのではないか。この発想に後押しされて、大規模で包括的な文字セット Unicode が誕生しました。

    -

    Unicode の中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理・表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。

    -

    1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。巨大な Unicode 文字セットでは、よく使われる文字は 2 バイトを占め、一部の珍しい文字は 3 バイト、さらには 4 バイトを占めます。

    -

    Unicode は汎用文字セットであり、本質的には各文字に番号(「コードポイント」)を割り当てるものですが、それらのコードポイントをコンピュータ内でどのように保存するかまでは規定していません。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか。

    +

    Unicode の中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理・表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。

    +

    Unicode は汎用文字セットとして、本質的には各文字に固有の「コードポイント」(文字番号)を割り当てており、その範囲は U+0000 から U+10FFFF までで、統一された文字番号空間を構成しています。しかし、Unicode はそれらのコードポイントをコンピュータ内でどのように保存するかまでは規定していません。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか。

    この問題に対して、**すべての文字を固定長の符号として保存する**という直接的な解決策があります。下図のように、「Hello」の各文字は 1 バイト、「アルゴリズム」の各文字は 2 バイトを占めます。上位ビットを 0 で埋めることで、「Hello アルゴリズム」のすべての文字を 2 バイト長にエンコードできます。こうすれば、システムは 2 バイトごとに 1 文字を解析して、この語句の内容を復元できます。

    Unicode エンコーディングの例

    図 3-7   Unicode エンコーディングの例

    diff --git a/ja/javascripts/animation_player.js b/ja/javascripts/animation_player.js index 1447a556a..b84b9f0ce 100644 --- a/ja/javascripts/animation_player.js +++ b/ja/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410024950 */ +/*! update cache: 20260410223953 */ diff --git a/ja/javascripts/katex.js b/ja/javascripts/katex.js index 2d000c38c..14ef9c31c 100644 --- a/ja/javascripts/katex.js +++ b/ja/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410024950 */ +/*! update cache: 20260410223953 */ diff --git a/ja/javascripts/mathjax.js b/ja/javascripts/mathjax.js index 88923a363..e3a00ec1e 100644 --- a/ja/javascripts/mathjax.js +++ b/ja/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260410024950 */ +/*! update cache: 20260410223953 */ diff --git a/ja/javascripts/starfield.js b/ja/javascripts/starfield.js index 2e1d0d5c2..fe7723828 100644 --- a/ja/javascripts/starfield.js +++ b/ja/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410024950 */ +/*! update cache: 20260410223953 */ diff --git a/ja/search.json b/ja/search.json index 8da394d83..b462e37b2 100644 --- a/ja/search.json +++ b/ja/search.json @@ -1 +1 @@ -{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"第 16 章   付録","text":"","path":["第 16 章   付録"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"章の内容","text":"
    • 16.1   プログラミング環境のインストール
    • 16.2   一緒に制作に参加しましょう
    • 16.3   用語集
    ","path":["第 16 章   付録"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   一緒に制作に参加しましょう","text":"

    著者の力には限りがあるため、本書にはどうしても一部の漏れや誤りが含まれる可能性があります。ご了承ください。誤字、リンク切れ、内容の欠落、表現の曖昧さ、説明の不明瞭さ、文章構成の不適切さなどの問題を見つけた場合は、ぜひ修正にご協力ください。読者により良い学習リソースを提供できます。

    すべての寄稿者の GitHub ID は、本書のリポジトリ、Web 版、PDF 版のホームページに掲載され、オープンソースコミュニティへの惜しみない貢献に感謝を表します。

    オープンソースの魅力

    紙の書籍では、2 回の増刷の間隔が長くなりがちで、内容更新は非常に不便です。

    一方、このオープンソース書籍では、内容更新のサイクルは数日、場合によっては数時間にまで短縮されています。

    ","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/contribution/#1","level":3,"title":"1.   内容の微調整","text":"

    以下の図のように、各ページの右上には「編集アイコン」があります。次の手順で本文やコードを修正できます。

    1. 「編集アイコン」をクリックし、「このリポジトリを Fork する必要があります」と表示された場合は、その操作を承認してください。
    2. Markdown のソースファイルを修正し、内容が正しいことを確認したうえで、できるだけ書式の統一を保ってください。
    3. ページ下部に修正内容の説明を入力し、その後「Propose file change」ボタンをクリックします。ページ遷移後、「Create 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. ローカルで行った変更を Commit し、その後リモートリポジトリへ Push します。
    5. リポジトリのページを更新し、「Create 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":"

    オープンソースで軽量な VS Code をローカルの統合開発環境(IDE)として使用することを推奨します。VS Code 公式サイト にアクセスし、使用している OS に応じたバージョンの VS Code をダウンロードしてインストールしてください。

    図 16-1   公式サイトから VS Code をダウンロード

    VS Code には強力な拡張機能のエコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。Python を例にすると、「Python Extension Pack」拡張機能をインストールした後、Python コードをデバッグできるようになります。インストール手順を以下に示します。

    図 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   データ構造とアルゴリズムの重要用語

    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 ビッグオー記法 asymptotic upper bound 漸近上界 sign-magnitude 符号絶対値表現 1’s complement 1の補数 2’s complement 2の補数 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 AVL 木 red-black tree 赤黒木 level-order traversal レベル順走査 breadth-first traversal 幅優先走査 depth-first traversal 深さ優先走査 binary search tree 二分探索木 balanced binary search tree 平衡二分探索木 balance factor 平衡係数 heap ヒープ max heap 最大ヒープ min heap 最小ヒープ priority queue 優先度付きキュー heapify ヒープ化 top-\\(k\\) problem Top-\\(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 章   配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"章の内容","text":"
    • 4.1   配列
    • 4.2   連結リスト
    • 4.3   リスト
    • 4.4   メモリとキャッシュ *
    • 4.5   まとめ
    ","path":["第 4 章   配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   配列","text":"

    配列(array)は線形データ構造の一種であり、同じ型の要素を連続したメモリ領域に格納します。要素が配列内にある位置を、その要素のインデックス(index)と呼びます。下図は、配列の主要な概念と格納方式を示しています。

    図 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":"

    必要に応じて、配列の初期化方法として初期値なしと初期値ありの 2 種類を使い分けられます。初期値を指定しない場合、多くのプログラミング言語では配列要素は \\(0\\) に初期化されます。

    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%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%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   配列要素のメモリアドレスの計算

    上図を見ると、配列の最初の要素のインデックスは \\(0\\) であり、これは少し直感に反するように思えます。というのも、\\(1\\) から数え始めるほうが自然だからです。しかし、アドレス計算式の観点では、**インデックスの本質はメモリアドレスのオフセット**です。先頭要素のアドレスのオフセットは \\(0\\) であるため、そのインデックスが \\(0\\) なのは妥当です。

    配列では要素へのアクセスは非常に効率的であり、\\(O(1)\\) 時間で任意の要素にランダムアクセスできます。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def random_access(nums: list[int]) -> int:\n    \"\"\"要素へランダムアクセス\"\"\"\n    # 区間 [0, len(nums)-1] からランダムに数字を 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) からランダムに 1 つの数を選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに数字を 1 つ選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに数字を 1 つ選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに 1 つの数を選ぶ\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()) からランダムに数字を 1 つ選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに数字を 1 つ選ぶ\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) からランダムに 1 つの数を選ぶ\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":"

    配列要素はメモリ内で「ぴったり隣接して」おり、その間にほかのデータを格納する余地はありません。下図のように、配列の途中に要素を挿入したい場合は、その要素より後ろにあるすべての要素を 1 つずつ後ろへずらし、その後でそのインデックスに要素を代入する必要があります。

    図 4-3   配列への要素挿入の例

    注意すべき点として、配列の長さは固定であるため、要素を 1 つ挿入すると配列末尾の要素が必ず「失われ」ます。この問題の解決策は「リスト」の章で扱います。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def insert(nums: list[int], num: int, index: int):\n    \"\"\"配列の index 番目に要素 num を挿入\"\"\"\n    # インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # index の要素に num を代入する\n    nums[index] = num\n
    array.cpp
    /* 配列の index 番目に要素 num を挿入 */\nvoid insert(int *nums, int size, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.java
    /* 配列の index 番目に要素 num を挿入 */\nvoid insert(int[] nums, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.cs
    /* 配列の index 番目に要素 num を挿入 */\nvoid Insert(int[] nums, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.go
    /* 配列の index 番目に要素 num を挿入 */\nfunc insert(nums []int, num int, index int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
    array.swift
    /* 配列の index 番目に要素 num を挿入 */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
    array.js
    /* 配列の index 番目に要素 num を挿入 */\nfunction insert(nums, num, index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.ts
    /* 配列の index 番目に要素 num を挿入 */\nfunction insert(nums: number[], num: number, index: number): void {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.dart
    /* 配列の添字 index に要素 _num を挿入 */\nvoid insert(List<int> nums, int _num, int index) {\n  // インデックス index 以降の全要素を 1 つ後ろへ移動する\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
    /* 配列の index 番目に要素 num を挿入 */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.c
    /* 配列の index 番目に要素 num を挿入 */\nvoid insert(int *nums, int size, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.kt
    /* 配列の index 番目に要素 num を挿入 */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
    array.rb
    ### 配列のインデックス index に要素 num を挿入 ###\ndef insert(nums, num, index)\n  # インデックス index 以降の全要素を 1 つ後ろへ移動する\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # index の要素に num を代入する\n  nums[index] = num\nend\n
    コードの可視化

    全画面で見る >

    ","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4","level":3,"title":"4.   要素の削除","text":"

    同様に、下図のように、インデックス \\(i\\) の要素を削除したい場合は、インデックス \\(i\\) より後ろの要素をすべて 1 つずつ前へずらす必要があります。

    図 4-4   配列からの要素削除の例

    注意してください。要素の削除が完了すると、もともとの末尾要素は「意味を持たない」状態になるため、わざわざ変更する必要はありません。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def remove(nums: list[int], index: int):\n    \"\"\"index の要素を削除する\"\"\"\n    # インデックス index より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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// JavaScript の Array は動的配列であり、直接拡張できます\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// TypeScript の Array は動的配列であり、直接拡張できます\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# Ruby の Array は動的配列であり、直接拡張できます\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 コード値をインデックスとし、対応する要素を配列の対応位置に格納できます。
    • 機械学習:ニューラルネットワークでは、ベクトル、行列、テンソル間の線形代数演算が大量に使われ、これらのデータはいずれも配列の形で構築されます。配列はニューラルネットワークプログラミングで最もよく使われるデータ構造です。
    • データ構造の実装:配列はスタック、キュー、ハッシュテーブル、ヒープ、グラフなどのデータ構造の実装に利用できます。たとえば、グラフの隣接行列表現は実際には 2 次元配列です。
    ","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   連結リスト","text":"

    メモリ空間はすべてのプログラムに共通の資源であり、複雑なシステム実行環境では、空きメモリがメモリの各所に散在している可能性があります。配列を格納するメモリ空間は連続していなければなりませんが、配列が非常に大きい場合、メモリはそのような大きな連続領域を提供できないことがあります。このとき、連結リストの柔軟性という利点が現れます。

    連結リスト(linked list)は線形データ構造の一種であり、各要素はノードオブジェクトです。各ノードは「参照」によって接続されます。参照には次のノードのメモリアドレスが記録されており、これによって現在のノードから次のノードへアクセスできます。

    連結リストの設計では、各ノードをメモリの各所に分散して格納でき、それらのメモリアドレスは連続している必要がありません。

    図 4-5   連結リストの定義と格納方式

    上図を見ると、連結リストの構成単位はノード(node)オブジェクトです。各ノードは 2 つのデータ、すなわちノードの「値」と次のノードを指す「参照」を含みます。

    • 連結リストの最初のノードを「先頭ノード」、最後のノードを「末尾ノード」と呼びます。
    • 末尾ノードが指す先は「空」であり、Java、C++、Python ではそれぞれ nullnullptrNone と表記します。
    • C、C++、Go、Rust などポインタをサポートする言語では、上記の「参照」は「ポインタ」に置き換えるべきです。

    以下のコードが示すように、連結リストノード ListNode は値のほかに、追加で 1 つの参照(ポインタ)を保持する必要があります。そのため、同じデータ量であれば、連結リストは配列より多くのメモリ空間を消費します。

    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":"

    連結リストの構築は 2 つの手順に分かれます。第 1 に各ノードオブジェクトを初期化し、第 2 にノード間の参照関係を構築します。初期化が完了したら、連結リストの先頭ノードから出発し、参照で 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%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%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%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%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

    配列全体は 1 つの変数であり、たとえば配列 nums には nums[0]nums[1] などの要素が含まれます。一方、連結リストは複数の独立したノードオブジェクトで構成されます。通常、先頭ノードを連結リストの代名詞として扱います。たとえば上記のコードの連結リストは n0 と表せます。

    ","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2","level":3,"title":"2.   ノードの挿入","text":"

    連結リストへのノード挿入は非常に簡単です。下図に示すように、隣り合う 2 つのノード n0n1 の間に新しいノード P を挿入したいとします。このとき 2 つのノードの参照(ポインタ)を変更するだけでよく、時間計算量は \\(O(1)\\) です。

    これに対して、配列に要素を挿入する時間計算量は \\(O(n)\\) であり、データ量が大きい場合の効率は低くなります。

    図 4-6   連結リストへのノード挿入例

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def insert(n0: ListNode, P: ListNode):\n    \"\"\"連結リストでノード n0 の後ろにノード P を挿入する\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
    linked_list.cpp
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.java
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.cs
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.go
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
    linked_list.swift
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
    linked_list.js
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.ts
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\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
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
    linked_list.rs
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\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
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.kt
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
    linked_list.rb
    ### 連結リストのノード n0 の後にノード _p を挿入 ###\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":"

    下図に示すように、連結リストでのノード削除も非常に簡単で、1 つのノードの参照(ポインタ)を変更するだけで済みます。

    なお、削除操作が完了した後もノード 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)\\) 時間でアクセスできます。これに対して連結リストでは、プログラムは先頭ノードから出発し、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-vs","level":2,"title":"4.2.2   配列 vs. 連結リスト","text":"

    次の表は、配列と連結リストの各種特徴と操作効率をまとめたものです。両者は互いに逆の格納戦略を採用しているため、各種性質や操作効率にも対照的な特徴が現れます。

    表 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":"

    下図に示すように、一般的な連結リストの種類は 3 つあります。

    • 単方向連結リスト:前述した通常の連結リストのことです。単方向連結リストのノードは、値と次のノードを指す参照の 2 つのデータを含みます。最初のノードを先頭ノード、最後のノードを末尾ノードと呼び、末尾ノードは空 None を指します。
    • 循環連結リスト:単方向連結リストの末尾ノードを先頭ノードへ向けると(先頭と末尾をつなぐと)、循環連結リストが得られます。循環連結リストでは、任意のノードを先頭ノードとみなせます。
    • 双方向連結リスト:単方向連結リストと比べて、双方向連結リストは 2 方向の参照を記録します。双方向連結リストのノード定義には、後続ノード(次のノード)と前駆ノード(前のノード)を指す参照(ポインタ)が含まれます。単方向連結リストより柔軟で、2 方向に連結リストを走査できますが、そのぶん多くのメモリ空間を必要とします。
    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":"

    単方向連結リストは、スタック、キュー、ハッシュテーブル、グラフなどのデータ構造の実装によく用いられます。

    • スタックとキュー:挿入と削除の両方の操作を連結リストの一端で行うと、その性質は後入れ先出しとなり、スタックに対応します。挿入を連結リストの一端で行い、削除をもう一端で行うと、その性質は先入れ先出しとなり、キューに対応します。
    • ハッシュテーブル:連鎖アドレス法はハッシュ衝突を解決する主流の方式の 1 つであり、この方式では、衝突したすべての要素が 1 つの連結リストに格納されます。
    • グラフ:隣接リストはグラフを表現する一般的な方法の 1 つであり、グラフの各頂点は 1 つの連結リストに関連付けられます。連結リスト内の各要素は、その頂点に接続されたほかの頂点を表します。

    双方向連結リストは、前後の要素をすばやく見つける必要がある場面でよく用いられます。

    • 高度なデータ構造:たとえば赤黒木や B 木では、ノードの親ノードへアクセスする必要があります。これは、ノード内に親ノードを指す参照を保持することで実現でき、双方向連結リストに似ています。
    • ブラウザ履歴:Web ブラウザでユーザーが進むボタンや戻るボタンをクリックしたとき、ブラウザはユーザーが訪れた前後のページを知る必要があります。双方向連結リストの性質によって、この操作は簡単になります。
    • LRU アルゴリズム:キャッシュ淘汰(LRU)アルゴリズムでは、最近最も使用されていないデータをすばやく見つける必要があり、さらにノードの高速な追加と削除も必要です。そのため、双方向連結リストが非常に適しています。

    循環連結リストは、オペレーティングシステムのリソーススケジューリングのように、周期的な操作が必要な場面でよく用いられます。

    • ラウンドロビン時間片スケジューリングアルゴリズム:オペレーティングシステムにおいて、ラウンドロビン時間片スケジューリングは一般的な CPU スケジューリングアルゴリズムであり、一連のプロセスを循環的に処理する必要があります。各プロセスには 1 つの時間片が割り当てられ、その時間片を使い切ると、CPU は次のプロセスへ切り替わります。この循環操作は、循環連結リストで実現できます。
    • データバッファ:一部のデータバッファ実装でも、循環連結リストが使われることがあります。たとえば音声・動画プレーヤーでは、データストリームを複数のバッファブロックに分割して循環連結リストへ格納し、シームレス再生を実現できます。
    ","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   リスト","text":"

    リスト(list)は抽象的なデータ構造の概念であり、要素の順序付き集合を表す。要素のアクセス、更新、追加、削除、走査などの操作をサポートし、利用者は容量制限の問題を考慮する必要がない。リストは連結リストまたは配列に基づいて実装できる。

    • 連結リストは本質的にリストと見なすことができ、要素の追加・削除・参照・更新をサポートし、柔軟に動的拡張できる。
    • 配列も要素の追加・削除・参照・更新をサポートするが、長さが不変であるため、長さ制限のあるリストとしか見なせない。

    配列でリストを実装する場合、長さが不変である性質によってリストの実用性が低下する。これは、通常は事前にどれだけのデータを格納する必要があるかを決められず、適切なリスト長を選びにくいためである。長さが小さすぎると利用要件を満たせない可能性が高く、大きすぎるとメモリ空間の浪費を招く。

    この問題を解決するために、動的配列(dynamic array)を用いてリストを実装できる。これは配列の各種利点を引き継ぎつつ、プログラム実行中に動的な拡張を行える。

    実際には、多くのプログラミング言語の標準ライブラリが提供するリストは動的配列に基づいて実装されている。たとえば、Python の list 、Java の ArrayList 、C++ の vector 、C# の List などである。以降の議論では、「リスト」と「動的配列」を同じ概念として扱う。

    ","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":"

    通常は「初期値なし」と「初期値あり」の 2 つの初期化方法を用いる。

    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// 初期値あり(配列の要素型は int[] のラッパークラスである Integer[] である必要があることに注意)\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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20%23%20%E6%97%A0%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%E8%AE%BF%E9%97%AE%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%0A%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%E5%B0%86%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%E6%9B%B4%E6%96%B0%E4%B8%BA%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)  # インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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:]...)...) // インデックス 3 に数値 6 を挿入\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) // インデックス 3 に数値 6 を挿入\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); // インデックス 3 に数値 6 を挿入\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); // インデックス 3 に数値 6 を挿入\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); // インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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) # インデックス 3 に数値 6 を挿入\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%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B8%85%E7%A9%BA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E5%B0%BE%E9%83%A8%E6%B7%BB%E5%8A%A0%E5%85%83%E7%B4%A0%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%E5%9C%A8%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.insert%283,%206%29%20%20%23%20%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0&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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%80%9A%E8%BF%87%E7%B4%A2%E5%BC%95%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%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%E7%9B%B4%E6%8E%A5%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%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
    # 2 つのリストを連結\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # リスト nums1 を nums の後ろに連結\n
    list.cpp
    /* 2 つのリストを連結 */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// リスト nums1 を nums の後ろに連結\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
    list.java
    /* 2 つのリストを連結 */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // リスト nums1 を nums の後ろに連結\n
    list.cs
    /* 2 つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // リスト nums1 を nums の後ろに連結\n
    list_test.go
    /* 2 つのリストを連結 */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // リスト nums1 を nums の後ろに連結\n
    list.swift
    /* 2 つのリストを連結 */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // リスト nums1 を nums の後ろに連結\n
    list.js
    /* 2 つのリストを連結 */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // リスト nums1 を nums の後ろに連結\n
    list.ts
    /* 2 つのリストを連結 */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // リスト nums1 を nums の後ろに連結\n
    list.dart
    /* 2 つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // リスト nums1 を nums の後ろに連結\n
    list.rs
    /* 2 つのリストを連結 */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
    list.c
    // C には組み込みの動的配列がない\n
    list.kt
    /* 2 つのリストを連結 */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // リスト nums1 を nums の後ろに連結\n
    list.rb
    # 2 つのリストを連結\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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8B%BC%E6%8E%A5%E4%B8%A4%E4%B8%AA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums1%20%3D%20%5B6,%208,%207,%2010,%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%20nums1%20%E6%8B%BC%E6%8E%A5%E5%88%B0%20nums%20%E4%B9%8B%E5%90%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/#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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8E%92%E5%BA%8F%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.sort%28%29%20%20%23%20%E6%8E%92%E5%BA%8F%E5%90%8E%EF%BC%8C%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E4%BB%8E%E5%B0%8F%E5%88%B0%E5%A4%A7%E6%8E%92%E5%88%97&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 などがある。それらの実装は比較的複雑で、初期容量や拡張倍率など各種パラメータの設定もよく考えられている。興味があればソースコードを参照して学べる。

    リストの動作原理への理解を深めるため、ここでは簡易版のリストを実装し、以下の 3 つの設計ポイントを含める。

    • 初期容量:妥当な配列の初期容量を選ぶ。この例では 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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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   コンピュータの記憶装置

    ハードディスク メモリ キャッシュ 用途 OS、プログラム、ファイルなどを長期保存 実行中のプログラムや処理中のデータを一時保存 頻繁にアクセスされるデータや命令を保存し、CPU のメモリアクセス回数を減らす 揮発性 電源断後もデータは失われない 電源断後にデータは失われる 電源断後にデータは失われる 容量 大きい、TB 級 小さい、GB 級 非常に小さい、MB 級 速度 遅い、数百〜数千 MB/s 速い、数十 GB/s 非常に速い、数十〜数百 GB/s 価格(人民元) 比較的安価、数角〜数元 / GB 比較的高価、数十〜数百元 / GB 非常に高価、CPU と一体で価格設定される

    コンピュータの記憶システムは、下図のようなピラミッド構造として捉えられます。ピラミッドの頂点に近い記憶装置ほど速度は速く、容量は小さく、コストは高くなります。この多層構造は偶然ではなく、コンピュータ科学者やエンジニアによる熟慮の末の設計です。

    • ハードディスクはメモリで置き換えにくい。まず、メモリ内のデータは電源断後に失われるため、長期保存には向きません。次に、メモリのコストはハードディスクの数十倍であり、消費者市場で広く普及しにくいという問題があります。
    • キャッシュは大容量と高速性を両立しにくい。L1、L2、L3 キャッシュの容量が段階的に増えるにつれて、物理サイズは大きくなり、CPU コアとの物理的距離も遠くなります。その結果、データ転送時間が増え、要素アクセスの遅延も大きくなります。現在の技術では、多層キャッシュ構造が容量、速度、コストの最適なバランスです。

    図 4-9   コンピュータの記憶システム

    Tip

    コンピュータの記憶階層は、速度、容量、コストの三者間にある巧妙なバランスを体現しています。実際、このようなトレードオフはあらゆる工業分野に広く存在しており、異なる利点と制約のあいだで最適な均衡点を見つけることが求められます。

    要するに、ハードディスクは大量データの長期保存に、メモリはプログラム実行中に処理しているデータの一時保存に、キャッシュは頻繁にアクセスされるデータや命令の保存に用いられ、プログラム実行効率を高めます。三者は協調して動作し、コンピュータシステムの高効率な運用を支えています。

    次の図に示すように、プログラム実行時にはデータがハードディスクからメモリへ読み込まれ、CPU の計算に使われます。キャッシュは CPU の一部と見なせ、メモリからデータを賢く読み込むことで、CPU に高速なデータ読み出しを提供し、プログラムの実行効率を大きく高め、低速なメモリへの依存を減らします。

    図 4-10   ハードディスク、メモリ、キャッシュ間のデータの流れ

    ","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)と呼び、この指標は通常、キャッシュ効率の評価に用いられます。

    できるだけ高い効率を実現するため、キャッシュは次のようなデータ読み込みの仕組みを採用しています。

    • キャッシュライン:キャッシュはデータを 1 バイト単位で保存・読み込みするのではなく、キャッシュライン単位で扱います。1 バイト単位の転送と比べて、キャッシュライン単位のほうが効率的です。
    • プリフェッチ機構:プロセッサはデータアクセスのパターン(たとえば順次アクセス、一定ステップ幅のスキップアクセスなど)を予測し、そのパターンに応じてデータをキャッシュへ読み込むことで、ヒット率を高めます。
    • 空間的局所性:あるデータがアクセスされた場合、その近傍のデータも近いうちにアクセスされる可能性があります。そのため、キャッシュはあるデータを読み込む際に、その周辺のデータもあわせて読み込み、ヒット率を高めます。
    • 時間的局所性:あるデータがアクセスされた場合、そのデータは近い将来に再びアクセスされる可能性が高いです。キャッシュはこの性質を利用し、最近アクセスしたデータを保持することでヒット率を高めます。

    実際には、配列と連結リストではキャッシュの利用効率が異なり、主に次の点に表れます。

    • 使用空間:連結リストの要素は配列要素より多くの空間を占めるため、キャッシュに収まる有効データ量は少なくなります。
    • キャッシュライン:連結リストのデータはメモリの各所に分散しており、キャッシュは「ライン単位で読み込む」ため、無効データまで読み込む割合が高くなります。
    • プリフェッチ機構:配列のほうが連結リストよりもデータアクセスのパターンを「予測しやすく」、システムが次に読み込まれるデータを推測しやすくなります。
    • 空間的局所性:配列はまとまったメモリ空間に格納されるため、読み込まれたデータの近くにあるデータも、まもなくアクセスされる可能性が高くなります。

    全体として、配列はより高いキャッシュヒット率を持つため、操作効率では通常、連結リストより優れています。このため、アルゴリズム問題を解く際には、配列ベースで実装されたデータ構造のほうが好まれることが多くなります。

    注意すべきなのは、**キャッシュ効率が高いからといって、配列があらゆる状況で連結リストより優れているとは限らない**という点です。実際にどのデータ構造を選ぶかは、具体的な要件に応じて決めるべきです。たとえば、配列と連結リストはいずれも「スタック」データ構造を実装できますが(次章で詳しく説明します)、適した場面は異なります。

    • アルゴリズム問題に取り組むときは、一般に配列ベースのスタックを選ぶ傾向があります。より高い操作効率とランダムアクセス能力を備えており、その代償は配列用に一定量のメモリを事前確保することだけです。
    • データ量が非常に大きく、動的性が高く、スタックの想定サイズを見積もりにくい場合は、連結リストベースのスタックのほうが適しています。連結リストなら大量のデータをメモリの異なる場所に分散して保存でき、配列拡張による追加コストも回避できます。
    ","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":"
    • 配列と連結リストは 2 種類の基本的なデータ構造であり、それぞれコンピュータメモリにおけるデータの 2 つの格納方式、すなわち連続領域への格納と分散領域への格納を表す。両者の特徴は相互補完的である。
    • 配列はランダムアクセスをサポートし、使用メモリも少ない。一方で、要素の挿入と削除の効率は低く、初期化後に長さを変更できない。
    • 連結リストは参照(ポインタ)を変更することでノードの挿入と削除を効率的に行え、長さも柔軟に調整できる。一方で、ノードへのアクセス効率は低く、メモリ使用量も多い。一般的な連結リストには単方向連結リスト、循環連結リスト、双方向連結リストがある。
    • リストは、追加・削除・検索・更新をサポートする順序付き要素集合であり、通常は動的配列に基づいて実装される。配列の利点を保ちながら、長さを柔軟に調整できる。
    • リストの登場により配列の実用性は大幅に高まったが、一部のメモリ領域が無駄になる可能性がある。
    • プログラムの実行時、データは主にメモリに格納される。配列はより高いメモリ空間効率を提供でき、連結リストはメモリ利用の面でより柔軟である。
    • キャッシュは、キャッシュライン、プリフェッチ機構、空間局所性と時間局所性といったデータ読み込み機構を通じて 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:なぜ配列では同じ型の要素が求められるのに、連結リストでは同じ型であることが強調されないのですか?

    連結リストはノードで構成され、ノード同士は参照(ポインタ)で接続されている。各ノードには intdoublestringobject など、異なる型のデータを格納できる。

    これに対して、配列要素は同じ型でなければならない。そうでなければ、オフセットを計算して対応する要素位置を取得できないからである。たとえば、配列に intlong の 2 種類が同時に含まれていて、各要素がそれぞれ 4 バイトと 8 バイトを占める場合、配列内に 2 種類の「要素長」が存在するため、次の式ではオフセットを計算できない。

    # 要素のメモリアドレス = 配列のメモリアドレス(先頭要素のメモリアドレス) + 要素長 * 要素インデックス\n

    Q:ノード P を削除した後、P.nextNone に設定する必要はありますか?

    P.next を変更しなくてもよい。この連結リストの観点では、先頭ノードから末尾ノードまでたどっても、もはや P に出会うことはない。つまり、ノード P はすでに連結リストから削除されており、この時点で P がどこを指していても、この連結リストには影響しない。

    データ構造とアルゴリズム(問題を解くとき)の観点では、切り離さなくても問題はなく、プログラムのロジックが正しいことを保証すればよい。標準ライブラリの観点では、切り離したほうがより安全で、ロジックも明確である。切り離さない場合、削除されたノードが適切に回収されなかったとすると、後続ノードのメモリ回収に影響する可能性がある。

    Q:連結リストでの挿入と削除の時間計算量は \\(O(1)\\) です。しかし、追加や削除の前には要素を探すのに \\(O(n)\\) の時間が必要です。では、なぜ時間計算量は \\(O(n)\\) ではないのですか?

    要素を先に探してから削除するのであれば、時間計算量が \\(O(n)\\) であるのは確かである。しかし、連結リストの \\(O(1)\\) での追加・削除という利点は、ほかの用途で生かせる。たとえば、両端キューは連結リストで実装するのに適しており、先頭ノードと末尾ノードを常に指すポインタ変数を維持すれば、各挿入・削除操作はどれも \\(O(1)\\) になる。

    Q:図「連結リストの定義と格納方式」で、薄青色のノードポインタ部分は 1 つのメモリアドレスを占めているのですか? それともノード値と半分ずつなのでしょうか?

    この模式図は定性的な表現にすぎず、定量的な表現は具体的な状況に応じて分析する必要がある。

    • ノード値が占める領域は型によって異なり、たとえば intlongdouble、インスタンスオブジェクトなどがある。
    • ポインタ変数が占めるメモリ空間の大きさは、使用する OS やコンパイル環境によって異なり、多くは 8 バイトまたは 4 バイトである。

    Q:リストの末尾への要素追加は常に \\(O(1)\\) ですか?

    要素を追加する際にリスト長を超える場合は、先にリストを拡張してから追加する必要がある。システムは新しいメモリ領域を確保し、元のリストの全要素をそこへ移動するため、このとき時間計算量は \\(O(n)\\) になる。

    Q:「リストの登場により配列の実用性は大きく向上したが、一部のメモリ空間が無駄になる可能性がある」というのは、容量、長さ、拡張倍率のような追加変数が占めるメモリのことですか?

    ここでいう空間の無駄には主に 2 つの意味がある。一方では、リストには初期長が設定されるが、必ずしもそれだけ必要とは限らない。もう一方では、頻繁な拡張を防ぐため、拡張時には通常ある係数、たとえば \\(\\times 1.5\\) を掛ける。このため、多くの空きスロットが生じ、通常それらを完全に埋めることはできない。

    Q:Python で n = [1, 2, 3] を初期化した後、この 3 つの要素のアドレスは連続しています。しかし m = [2, 1, 3] を初期化すると、各要素の id は連続しておらず、それぞれ n 内の同じ値と一致していることがわかります。これらの要素のアドレスが連続していないなら、m も配列なのですか?

    仮にリスト要素を連結リストのノード n = [n1, n2, n3, n4, n5] に置き換えたとしても、通常この 5 つのノードオブジェクトもメモリ上の各所に分散して格納される。それでも、与えられたリストインデックスに対して、私たちは依然として \\(O(1)\\) 時間でノードのメモリアドレスを取得し、対応するノードにアクセスできる。これは、配列に格納されているのがノードそのものではなく、ノードへの参照だからである。

    多くの言語と異なり、Python では数値もオブジェクトとしてラップされており、リストに格納されているのは数値そのものではなく、数値への参照である。そのため、2 つの配列内の同じ数値が同一の id を持つことがあり、しかもそれらの数値のメモリアドレスは連続している必要がない。

    Q:C++ STL の std::list はすでに双方向連結リストを実装していますが、アルゴリズム本ではあまり直接使われないようです。何か制約があるのでしょうか?

    一方では、私たちは多くの場合、アルゴリズムの実装に配列を好み、必要なときにだけ連結リストを使う。その主な理由は 2 つある。

    • 空間オーバーヘッド:各要素には 2 つの追加ポインタ(前の要素用と次の要素用)が必要なため、std::list は通常 std::vector より多くの空間を消費する。
    • キャッシュ非効率:データが連続して格納されていないため、std::list はキャッシュの利用効率が低い。一般には、std::vector のほうが性能がよい。

    もう一方では、連結リストを使う必要がある代表的な場面は主に二分木とグラフである。スタックやキューについては、連結リストではなく、たいてい言語が提供する stackqueue を使う。

    Q:res = [[0]] * n という操作で 2 次元リストを生成した場合、それぞれの [0] は独立していますか?

    独立していない。この 2 次元リストでは、すべての [0] は実際には同一オブジェクトへの参照である。そのうちの 1 つを変更すると、対応するすべての要素が一緒に変化することがわかる。

    2 次元リスト内の各 [0] を独立させたい場合は、res = [[0] for _ in range(n)] を使って実現できる。この方式の原理は、独立した [0] リストオブジェクトを \\(n\\) 個初期化していることにある。

    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 章   バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"章の内容","text":"
    • 13.1   バックトラッキングアルゴリズム
    • 13.2   全順列問題
    • 13.3   部分和問題
    • 13.4   n クイーン問題
    • 13.5   まとめ
    ","path":["第 13 章   バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   バックトラッキングアルゴリズム","text":"

    バックトラッキングアルゴリズム(backtracking algorithm)は、総当たりによって問題を解く手法です。その中核となる考え方は、初期状態から出発し、あり得るすべての解を力任せに探索し、正しい解に到達したらそれを記録し、解を見つけるか、考えられるすべての選択を試しても解が見つからなくなるまで続ける、というものです。

    バックトラッキングアルゴリズムでは、通常「深さ優先探索」を用いて解空間をたどります。「二分木」の章で述べたように、前順・中順・後順走査はいずれも深さ優先探索に属します。ここでは前順走査を使ってバックトラッキング問題を構成し、その仕組みを段階的に理解していきます。

    例題1

    1 本の二分木が与えられたとき、値が \\(7\\) のノードをすべて探索して記録し、そのノードのリストを返してください。

    この問題では、この木を前順走査し、現在のノードの値が \\(7\\) かどうかを判定します。該当する場合は、そのノードの値を結果リスト res に追加します。関連する処理は下図と次のコードのとおりです。

    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では、各ノードへの訪問が 1 回の「試行」に対応し、葉ノードを越えるか親ノードへ戻る return は「戻る」を表します。

    ここで強調しておきたいのは、**戻るとは関数の 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 から取り除き、**今回の試行前の状態を復元する**必要があります。

    次の図に示す過程を見ると、試行と戻るは「前進」と「取り消し」として理解できます。この 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":"

    複雑なバックトラッキング問題には、通常 1 つ以上の制約条件が含まれます。制約条件は多くの場合「枝刈り」に利用できます。

    例題3

    二分木の中で値が \\(7\\) のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。ただし、経路には値が \\(3\\) のノードを含めてはいけません。

    上の制約条件を満たすために、枝刈り操作を追加する必要があります。探索中に値が \\(3\\) のノードに出会った場合は、そこで早めに return し、それ以上探索を続けません。コードは次のとおりです。

    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   制約条件にもとづく枝刈り

    ","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 文は削除しなければなりません。次の図は、return 文を残す場合と削除する場合の探索過程を比較したものです。

    図 13-4   return を残す場合と削除する場合の探索過程の比較

    前順走査にもとづく実装と比べると、バックトラッキングアルゴリズムのフレームワークにもとづく実装はやや冗長に見えますが、汎用性に優れています。実際、多くのバックトラッキング問題はこのフレームワークで解けます。具体的な問題に応じて statechoices を定義し、各メソッドを実装すれば十分です。

    ","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   よく使われる用語","text":"

    アルゴリズム問題をより明確に分析するために、バックトラッキングでよく使われる用語の意味を整理し、例題3に対応する例を次の表にまとめます。

    表 13-1   よく使われるバックトラッキング用語

    用語 定義 例題3 解(solution) 問題の特定の条件を満たす答えであり、1 つまたは複数存在し得る 根ノードからノード \\(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":"

    バックトラッキングアルゴリズムの本質は深さ優先探索です。条件を満たす解を見つけるまで、あり得るすべての解を試します。この方法の利点は、考えられるすべての解を見つけられることであり、適切な枝刈りを行えば高い効率を発揮します。

    しかし、大規模または複雑な問題を扱う場合、バックトラッキングアルゴリズムの実行効率は受け入れがたいことがあります。

    • 時間:バックトラッキングアルゴリズムでは通常、状態空間のすべての可能性をたどる必要があり、時間計算量は指数時間や階乗時間に達することがあります。
    • 空間:再帰呼び出しの過程では現在の状態(たとえば経路や枝刈り用の補助変数など)を保持する必要があり、深さが大きいと空間使用量も大きくなります。

    それでもなお、バックトラッキングアルゴリズムは一部の探索問題や制約充足問題に対する最良の解法です。この種の問題では、どの選択が有効な解を生むかを事前に予測できないため、可能な選択肢をすべてたどる必要があります。このときの鍵は**いかに効率を最適化するか**であり、代表的な方法は 2 つあります。

    • 枝刈り:解が生じないことが確実な経路を探索しないことで、時間と空間を節約する。
    • ヒューリスティック探索:探索中に何らかの戦略や推定値を導入し、有効な解を生みやすい経路を優先的に探索する。
    ","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316","level":2,"title":"13.1.6   バックトラッキングの典型例題","text":"

    バックトラッキングアルゴリズムは、多くの探索問題、制約充足問題、組合せ最適化問題の解決に利用できます。

    探索問題:この種の問題の目標は、特定の条件を満たす解を見つけることです。

    • 全順列問題:ある集合が与えられたとき、考えられるすべての順列を求める。
    • 部分和問題:ある集合と目標和が与えられたとき、和が目標値となるすべての部分集合を見つける。
    • ハノイの塔問題:3 本の柱と大きさの異なる複数の円盤が与えられたとき、すべての円盤を 1 本の柱から別の柱へ移動する。ただし 1 回に 1 枚しか動かせず、大きい円盤を小さい円盤の上に置いてはならない。

    制約充足問題:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。

    • \\(n\\) クイーン問題:\\(n \\times n\\) の盤面に \\(n\\) 個のクイーンを配置し、互いに攻撃し合わないようにする。
    • 数独:\\(9 \\times 9\\) のグリッドに数字 \\(1\\) ~ \\(9\\) を入れ、各行・各列・各 \\(3 \\times 3\\) の小区画で数字が重複しないようにする。
    • グラフ彩色問題:無向グラフが与えられたとき、隣接する頂点が同じ色にならないように、できるだけ少ない色で各頂点を彩色する。

    組合せ最適化問題:この種の問題の目標は、組合せ空間の中で条件を満たす最適解を見つけることです。

    • 0-1 ナップサック問題:複数の品物とナップサックが与えられ、各品物には価値と重さがある。ナップサック容量の範囲内で総価値が最大になるように品物を選ぶ。
    • 巡回セールスマン問題:グラフ内のある頂点から出発し、他のすべての頂点をちょうど 1 回ずつ訪れて出発点へ戻るときの最短経路を求める。
    • 最大クリーク問題:無向グラフが与えられたとき、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。

    多くの組合せ最適化問題では、バックトラッキングは最適な解法ではない点に注意してください。

    • 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\\) サイズの盤面が与えられたとき、すべてのクイーンが互いに攻撃し合わない配置を求めます。

    下図に示すように、\\(n = 4\\) のとき、2 つの解を見つけることができます。バックトラッキングの観点から見ると、\\(n \\times n\\) サイズの盤面には合計 \\(n^2\\) 個のマスがあり、これがすべての選択肢 choices を与えます。クイーンを 1 つずつ配置していく過程で、盤面の状態は絶えず変化し、その各時点の盤面が状態 state です。

    図 13-15   4 クイーン問題の解

    下図は本問題の 3 つの制約条件を示しています。複数のクイーンは同じ行、同じ列、同じ対角線上に置けません。なお、対角線には主対角線 \\ と副対角線 / の 2 種類があります。

    図 13-16   n クイーン問題の制約条件

    ","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1","level":3,"title":"1.   行ごとの配置戦略","text":"

    クイーンの数と盤面の行数はいずれも \\(n\\) なので、次の推論を容易に得られます:盤面の各行にはクイーンを 1 つだけ配置できます。

    つまり、行ごとの配置戦略を採用できます:最初の行から始めて、各行に 1 つのクイーンを配置し、最後の行まで進みます。

    下図は 4 クイーン問題における行ごとの配置過程を示しています。図の大きさの都合上、下図では 1 行目における検索分岐の 1 つだけを展開し、列制約と対角線制約を満たさない案はすべて枝刈りしています。

    図 13-17   行ごとの配置戦略

    本質的には、行ごとの配置戦略は枝刈りとして機能します。これにより、同じ行に複数のクイーンが現れるすべての探索分岐を回避できます。

    ","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2","level":3,"title":"2.   列と対角線の枝刈り","text":"

    列制約を満たすために、長さ \\(n\\) のブール配列 cols を用いて、各列にクイーンがあるかどうかを記録できます。配置を決めるたびに、cols を使って既存のクイーンがある列を枝刈りし、バックトラッキングの中で cols の状態を動的に更新します。

    Tip

    注意として、行列の原点は左上にあり、行インデックスは上から下へ、列インデックスは左から右へ増加します。

    では、対角線制約はどのように扱えばよいのでしょうか。盤面上のあるマスの行列インデックスを \\((row, col)\\) とし、行列内のある主対角線を選ぶと、その対角線上のすべてのマスで行インデックスから列インデックスを引いた値が等しいことが分かります。つまり、主対角線上のすべてのマスでは \\(row - col\\) が一定値になります。

    つまり、2 つのマスが \\(row_1 - col_1 = row_2 - col_2\\) を満たすなら、それらは必ず同じ主対角線上にあります。この性質を利用して、下図の配列 diags1 により、各主対角線にクイーンがあるかどうかを記録できます。

    同様に、副対角線上のすべてのマスでは \\(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\\) であり、配列 diags1diags2 の長さもともに \\(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\\) 回配置し、列制約を考慮すると、1 行目から最終行までの選択肢はそれぞれ \\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 個となるため、時間計算量は \\(O(n!)\\) です。解を記録する際には、行列 state をコピーして res に追加する必要があり、このコピー操作には \\(O(n^2)\\) 時間を要します。したがって、全体の時間計算量は \\(O(n! \\cdot n^2)\\) です。実際には、対角線制約による枝刈りも探索空間を大きく縮小できるため、探索効率はしばしば上記の時間計算量より良くなります。

    配列 state は \\(O(n^2)\\) の空間を使用し、配列 colsdiags1diags2 はいずれも \\(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   全順列の例

    入力配列 すべての順列 \\([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]\\) が得られます。戻る操作は 1 つの選択を取り消し、その後で別の選択を試し続けることを表します。

    バックトラッキングコードの観点では、候補集合 choices は入力配列中のすべての要素であり、状態 state は現時点までに選ばれた要素です。各要素は 1 回しか選べないことに注意してください。したがって state 内の要素はすべて一意でなければなりません。

    下図のように、探索過程は再帰木として展開できます。木の各ノードは現在の状態 state を表します。根ノードから始めて 3 ラウンドの選択を経て葉ノードに到達し、各葉ノードが 1 つの順列に対応します。

    図 13-5   全順列の再帰木

    ","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1","level":3,"title":"1.   重複選択の枝刈り","text":"

    各要素が 1 回しか選ばれないようにするため、ブール配列 selected の導入を考えます。ここで selected[i]choices[i] がすでに選ばれているかどうかを表し、これに基づいて次の枝刈りを行います。

    • 選択 choice[i] を行った後、selected[i] を \\(\\text{True}\\) に設定し、その要素が選択済みであることを表します。
    • 選択肢リスト choices を走査するとき、すでに選ばれたノードはすべてスキップします。これが枝刈りです。

    下図のように、1 回目に 1、2 回目に 3、3 回目に 2 を選ぶとします。このとき 2 回目では要素 1 の分岐を、3 回目では要素 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]\\) だと仮定します。2 つの重複する要素 \\(1\\) を区別しやすくするため、2 つ目の \\(1\\) を \\(\\hat{1}\\) と記します。

    下図のように、上述の方法で生成される順列の半分は重複しています。

    図 13-7   重複した順列

    では、重複した順列をどのように取り除けばよいのでしょうか。最も直接的なのは、ハッシュ集合を用いて順列結果をそのまま重複排除する方法です。しかしこのやり方は十分に洗練されていません。なぜなら、重複順列を生成する探索分岐はそもそも不要であり、事前に見つけて枝刈りすべきだからです。そうすることで、アルゴリズム効率をさらに高められます。

    ","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1_1","level":3,"title":"1.   等しい要素の枝刈り","text":"

    下図を見ると、1 回目のラウンドでは \\(1\\) を選ぶことと \\(\\hat{1}\\) を選ぶことは等価であり、これら 2 つの選択の下で生成される順列はすべて重複します。したがって \\(\\hat{1}\\) を枝刈りすべきです。

    同様に、1 回目で \\(2\\) を選んだ後では、2 回目のラウンドにおける \\(1\\) と \\(\\hat{1}\\) も重複分岐を生むため、2 回目の \\(\\hat{1}\\) も枝刈りすべきです。

    本質的には、各ラウンドの選択において、等しい複数の要素が 1 回しか選ばれないようにすることが目標です。

    図 13-8   重複順列の枝刈り

    ","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2_1","level":3,"title":"2.   コード実装","text":"

    前問のコードを土台として、各ラウンドの選択でハッシュ集合 duplicated を 1 つ用意し、そのラウンドですでに試した要素を記録して、重複要素を枝刈りすることを考えます。

    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)\\) 空間を使用します。同時刻に存在する duplicated は最大で \\(n\\) 個であり、\\(O(n^2)\\) 空間を要します。したがって空間計算量は \\(O(n^2)\\) です。

    ","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3-2","level":3,"title":"3.   2 種類の枝刈りの比較","text":"

    selectedduplicated はどちらも枝刈りに用いられますが、目的は異なる点に注意してください。

    • 重複選択の枝刈り:探索全体を通して selected は 1 つだけです。これは現在の状態にどの要素が含まれているかを記録し、ある要素が state に重複して現れるのを防ぎます。
    • 等しい要素の枝刈り:各ラウンドの選択、すなわち各回の backtrack 呼び出しには duplicated が含まれます。これはそのラウンドの走査(for ループ)でどの要素がすでに選ばれたかを記録し、等しい要素が 1 回しか選ばれないことを保証します。

    下図は、2 つの枝刈り条件が有効になる範囲を示しています。木の各ノードは 1 つの選択を表し、根ノードから葉ノードまでの経路上の各ノードが 1 つの順列を構成することに注意してください。

    図 13-9   2 種類の枝刈り条件の作用範囲

    ","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\\}\\) です。次の 2 点に注意してください。

    • 入力集合内の要素は何度でも繰り返し選択できます。
    • 部分集合では要素の順序を区別しません。例えば \\(\\{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; // 解の個数を 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]\\) が含まれています。

    これは、探索過程では選択順を区別する一方で、部分集合では選択順を区別しないためです。次の図のように、先に \\(4\\) を選んでから \\(5\\) を選ぶ場合と、先に \\(5\\) を選んでから \\(4\\) を選ぶ場合は別の分岐ですが、対応する部分集合は同じです。

    図 13-10   部分集合探索と境界超過の枝刈り

    重複する部分集合を取り除くために、**直接的な方法として結果リストの重複を除去する**ことが考えられます。しかし、この方法は効率が低く、その理由は次の 2 点です。

    • 配列要素が多い場合、特に target が大きい場合には、探索過程で大量の重複部分集合が生成されます。
    • 部分集合(配列)同士の違いを比較するのは非常に時間がかかり、まず配列をソートし、その後に各要素を比較する必要があります。
    ","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2","level":3,"title":"2.   重複部分集合の枝刈り","text":"

    **探索過程で枝刈りを行って重複を除去する**ことを考えます。次の図を観察すると、重複部分集合は配列要素を異なる順序で選択したときに生じます。例えば次のような状況です。

    1. 1 回目と 2 回目でそれぞれ \\(3\\) と \\(4\\) を選ぶと、これら 2 要素を含むすべての部分集合、すなわち \\([3, 4, \\dots]\\) が生成されます。
    2. その後、1 回目で \\(4\\) を選んだ場合、**2 回目では \\(3\\) をスキップすべき**です。というのも、この選択で生成される部分集合 \\([4, 3, \\dots]\\) は、手順 1. で生成された部分集合と完全に重複するからです。

    探索過程では、各階層の選択は左から右へ順に試されるため、右側にある分岐ほど多く枝刈りされます。

    1. 最初の 2 回で \\(3\\) と \\(5\\) を選ぶと、部分集合 \\([3, 5, \\dots]\\) が生成されます。
    2. 最初の 2 回で \\(4\\) と \\(5\\) を選ぶと、部分集合 \\([4, 5, \\dots]\\) が生成されます。
    3. もし 1 回目で \\(5\\) を選ぶなら、**2 回目では \\(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\\) を満たし、部分集合の一意性が保証されます。

    これに加えて、コードには次の 2 つの最適化も施しています。

    • 探索を始める前に、まず配列 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
    コードの可視化

    全画面で見る >

    次の図は、配列 \\([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 に等しくなるすべての組合せを見つけてください。与えられた配列には重複要素が含まれる可能性があり、各要素は 1 回しか選択できません。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。

    前問と比べると、この問題の入力配列には重複要素が含まれる可能性があります。そのため、新たな問題が生じます。例えば、配列 \\([4, \\hat{4}, 5]\\) と目標値 \\(9\\) が与えられると、既存コードの出力は \\([4, 5], [\\hat{4}, 5]\\) となり、重複部分集合が現れます。

    この重複が生じる原因は、同じ値の要素があるラウンドで複数回選ばれてしまうことにあります。次の図では、1 回目には 3 つの選択肢があり、そのうち 2 つはどちらも \\(4\\) です。これにより 2 本の重複した探索分岐が生じ、重複部分集合が出力されます。同様に、2 回目の 2 つの \\(4\\) も重複部分集合を生みます。

    図 13-13   等しい要素によって生じる重複部分集合

    ","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1_1","level":3,"title":"1.   等しい要素の枝刈り","text":"

    この問題を解決するには、各ラウンドで等しい要素が 1 回しか選ばれないように制限する必要があります。実装方法は巧妙です。配列はすでにソートされているため、等しい要素は必ず隣り合っています。したがって、あるラウンドの選択で現在の要素が左隣の要素と等しいなら、それはすでに選ばれたことを意味するので、その要素を直接スキップします。

    同時に、**この問題では各配列要素を 1 回しか選択できない**という制約もあります。幸い、この制約も変数 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
    コードの可視化

    全画面で見る >

    次の図は、配列 \\([4, 4, 5]\\) と目標値 \\(9\\) に対するバックトラッキング過程を示しており、全部で 4 種類の枝刈り操作が含まれています。図とコードコメントを対応させながら、探索全体の流れと、各枝刈り操作がどのように機能するかを理解してください。

    図 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":"
    • バックトラッキングアルゴリズムの本質は全探索法であり、解空間を深さ優先で走査することで条件を満たす解を探索します。探索の過程で条件を満たす解に出会ったら記録し、すべての解を見つけるか探索が完了するまで続けます。
    • バックトラッキングアルゴリズムの探索過程は、試行と戻るという 2 つの部分から成ります。深さ優先探索によってさまざまな選択を試し、制約条件を満たさない状況に遭遇した場合は直前の選択を取り消して前の状態に戻り、ほかの選択を引き続き試します。試行と戻るは互いに逆方向の操作です。
    • バックトラッキング問題には通常複数の制約条件が含まれており、それらを枝刈りに利用できます。枝刈りによって不要な探索分岐を早期に打ち切り、探索効率を大幅に高められます。
    • バックトラッキングアルゴリズムは主に探索問題と制約充足問題の解決に用いられます。組合せ最適化問題もバックトラッキングで解けますが、より高効率またはより適した解法が存在することが少なくありません。
    • 全順列問題の目的は、与えられた集合要素のすべての可能な並べ方を探索することです。各要素が選択済みかどうかを配列で記録し、同じ要素を重複して選ぶ探索分岐を刈り取ることで、各要素が 1 度だけ選ばれるようにします。
    • 全順列問題では、集合内に重複要素があると最終結果にも重複した順列が現れます。各ラウンドで等しい要素は 1 回しか選べないように制約する必要があり、通常はハッシュ集合を用いて実現します。
    • 部分和問題の目標は、与えられた集合の中から和が目標値となるすべての部分集合を見つけることです。集合では要素順序を区別しませんが、探索過程では順序違いの結果も出力されるため、重複部分集合が生じます。そこで、バックトラッキング前にデータをソートし、各ラウンドの走査開始位置を示す変数を設定することで、重複部分集合を生成する探索分岐を枝刈りします。
    • 部分和問題では、配列中の等しい要素が重複集合を生みます。配列がソート済みであるという前提を利用し、隣接要素が等しいかどうかを判定して枝刈りすることで、等しい要素が各ラウンドで 1 回しか選ばれないようにします。
    • \\(n\\) クイーン問題の目的は、\\(n \\times n\\) の盤面に \\(n\\) 個のクイーンを配置する方法を見つけることであり、どの 2 つのクイーンも互いに攻撃できないことが条件です。この問題の制約には行制約、列制約、主対角線制約、副対角線制約があります。行制約を満たすため、行ごとに配置する戦略を採用し、各行に 1 個のクイーンを置くことを保証します。
    • 列制約と対角線制約の扱い方は似ています。列制約については、各列にクイーンが存在するかどうかを配列で記録し、選択したマスが有効かどうかを判定します。対角線制約については、主対角線と副対角線それぞれにクイーンが存在するかを 2 つの配列で記録します。難点は、同じ主対角線または副対角線上にあるマスが満たす行列インデックスの規則を見つけることにあります。
    ","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q:バックトラッキングと再帰の関係はどのように理解すればよいですか?

    全体として見ると、バックトラッキングは「アルゴリズム戦略」の一種であり、再帰はむしろ「道具」に近いものです。

    • バックトラッキングアルゴリズムは通常、再帰に基づいて実装されます。ただし、バックトラッキングは再帰の応用場面の 1 つであり、探索問題における再帰の応用です。
    • 再帰の構造は「部分問題への分解」という問題解決パラダイムを表しており、分割統治、バックトラッキング、動的計画法(メモ化再帰)などの問題によく用いられます。
    ","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章   計算量解析","text":"

    Abstract

    計算量解析は、広大なアルゴリズム宇宙における時空の案内人のようなものです。

    それは、時間と空間という二つの次元で私たちをより深く探求へ導き、より洗練された解決策を見つけ出します。

    ","path":["第 2 章   計算量解析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"章の内容","text":"
    • 2.1   アルゴリズム効率の評価
    • 2.2   反復と再帰
    • 2.3   時間計算量
    • 2.4   空間計算量
    • 2.5   まとめ
    ","path":["第 2 章   計算量解析"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   反復と再帰","text":"

    アルゴリズムでは、ある処理を繰り返し実行することがよくあり、これは複雑度解析と密接に関係しています。そのため、時間計算量と空間計算量を紹介する前に、まずプログラム内で反復実行を実現する方法、つまり 2 つの基本的な制御構造である反復と再帰について見ていきます。

    ","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 つで、反復回数があらかじめ分かっている場合に適しています。

    次の関数は for ループを用いて \\(1 + 2 + \\dots + n\\) の総和を計算しており、その結果は変数 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 ループも反復を実現する方法の 1 つです。while ループでは、各反復のたびにまず条件を確認し、条件が真であれば実行を続け、そうでなければループを終了します。

    次に、while ループを使って \\(1 + 2 + \\dots + n\\) の総和を求めてみましょう。

    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\\) が各反復で 2 回更新されており、このようなケースは for ループではあまり扱いやすくありません。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def while_loop_ii(n: int) -> int:\n    \"\"\"while ループ(2回更新)\"\"\"\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2 回更新)###\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":"

    1 つのループ構造の中に別のループ構造を入れ子にできます。以下では 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\\) と「二次関係」にあります。

    さらにネストしたループを追加することもできます。ネストが 1 段増えるたびに「次元が 1 つ上がる」ことになり、時間計算量は「三次関係」「四次関係」へと高くなっていきます。

    ","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222","level":2,"title":"2.2.2   再帰","text":"

    再帰(recursion)は、関数が自分自身を呼び出すことで問題を解決するアルゴリズム戦略です。主に 2 つの段階から成ります。

    1. 再帰呼び出し:プログラムは自分自身をより深く呼び出し続け、通常はより小さい、またはより単純化された引数を渡し、「終了条件」に達するまで進みます。
    2. 復帰: 「終了条件」が満たされると、プログラムは最も深い再帰関数から 1 層ずつ戻り、各層の結果をまとめていきます。

    実装の観点から見ると、再帰コードは主に 3 つの要素から成ります。

    1. 終了条件:いつ再帰呼び出しから復帰へ切り替わるかを決めます。
    2. 再帰呼び出し:再帰呼び出しに対応し、関数が自分自身を呼び出します。通常はより小さい、またはより単純化された引数を入力します。
    3. 結果の返却:復帰に対応し、現在の再帰レベルの結果を 1 つ上の層へ返します。

    次のコードを見ると、関数 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 つのパラダイムを表しています。

    • 反復:「ボトムアップ」で問題を解決します。最も基本的な手順から始め、それらを繰り返したり積み上げたりして、処理が完了するまで進めます。
    • 再帰:「トップダウン」で問題を解決します。元の問題をより小さな部分問題に分解し、それらの部分問題は元の問題と同じ形を持ちます。さらに部分問題をより小さな部分問題へと分解し、基本ケースに達したところで停止します(基本ケースの解は既知です)。

    前述の総和関数を例に、問題を \\(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 つの結果が生じます。

    • 関数のコンテキストデータは「スタックフレーム領域」と呼ばれるメモリ領域に保存され、関数が戻るまで解放されません。したがって、再帰は通常、反復より多くのメモリ空間を消費します。
    • 再帰による関数呼び出しには追加のオーバーヘッドが発生します。そのため再帰は通常、ループより時間効率が低くなります。

    次の図のように、終了条件が発動する前には、まだ戻っていない再帰関数が同時に \\(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 つ上の階層の関数へ戻った後も、引き続きコードを実行する必要があるため、システムは 1 つ上の呼び出しのコンテキストを保存しておく必要があります。
    • 末尾再帰:再帰呼び出しが関数の返却前の最後の操作であるため、1 つ上の階層へ戻った後に他の処理を続ける必要がなく、システムは 1 つ上の関数のコンテキストを保存する必要がありません。

    \\(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   末尾再帰の過程

    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)\\) とすると、次の 2 つが容易に分かります。

    • 数列の最初の 2 項は \\(f(1) = 0\\) と \\(f(2) = 1\\) です。
    • 数列中の各項は直前の 2 項の和であり、すなわち \\(f(n) = f(n - 1) + f(n - 2)\\) です。

    漸化式に従って再帰呼び出しを行い、最初の 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 回の再帰呼び出しを行っています。これは 1 回の呼び出しから 2 つの呼び出し分岐が生じることを意味します。次の図のように、この再帰呼び出しを繰り返していくと、最終的に深さ \\(n\\) の再帰木(recursion tree)が生成されます。

    図 2-6   フィボナッチ数列の再帰木

    本質的に見ると、再帰は「問題をより小さな部分問題へ分解する」という思考パラダイムを体現しており、この分割統治の戦略は非常に重要です。

    • アルゴリズムの観点では、探索、ソート、バックトラッキング、分割統治、動的計画法など、多くの重要な戦略が直接または間接にこの考え方を用いています。
    • データ構造の観点では、再帰は連結リスト、木、グラフに関する問題の処理に本質的に適しており、これらは分割統治の考え方で分析しやすいからです。
    ","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223","level":2,"title":"2.2.3   両者の比較","text":"

    以上をまとめると、次の表のように、反復と再帰は実装、性能、適用性の面で違いがあります。

    表 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
    コードの可視化

    全画面で見る >

    上のコードを見ると、再帰を反復へ変換すると、コードはより複雑になります。反復と再帰は多くの場合に相互変換できますが、常にそうする価値があるとは限りません。理由は次の 2 点です。

    • 変換後のコードは理解しにくくなり、可読性が下がる可能性があります。
    • 複雑な問題によっては、システムの呼び出しスタックの振る舞いを模擬すること自体が非常に難しい場合があります。

    要するに、反復を選ぶか再帰を選ぶかは、対象となる問題の性質によって決まります。実際のプログラミングでは、両者の長所と短所を見極め、状況に応じて適切な方法を選ぶことが重要です。

    ","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   アルゴリズム効率の評価","text":"

    アルゴリズム設計では、次の 2 つのレベルの目標を順に追求します。

    1. 問題の解法を見つける:アルゴリズムは、定められた入力範囲内で問題の正しい解を確実に求められる必要があります。
    2. 最適な解法を追求する:同じ問題に対して複数の解法が存在する場合があり、私たちはできるだけ効率的なアルゴリズムを見つけたいと考えます。

    つまり、問題を解けることを前提として、アルゴリズム効率はその良し悪しを測る主要な評価指標となっており、次の 2 つの観点を含みます。

    • 時間効率:アルゴリズムの実行時間の長さ。
    • 空間効率:アルゴリズムが使用するメモリ空間の大きさ。

    簡単に言えば、**私たちの目標は「高速で省メモリ」なデータ構造とアルゴリズムを設計すること**です。そして、アルゴリズム効率を効果的に評価することは非常に重要です。そうすることで初めて、さまざまなアルゴリズムを比較し、さらにアルゴリズム設計と最適化の過程を導けるからです。

    効率の評価方法は主に 2 種類に分けられます。実測と理論的な見積もりです。

    ","path":["第 2 章   計算量解析","2.1   アルゴリズム効率の評価"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211","level":2,"title":"2.1.1   実測","text":"

    いまアルゴリズム A とアルゴリズム B があり、どちらも同じ問題を解けるとします。この 2 つのアルゴリズムの効率を比較する必要がある場合、最も直接的な方法は 1 台のコンピュータで両者を実行し、その実行時間とメモリ使用量を監視して記録することです。この評価方法は実際の状況を反映できますが、大きな制約もあります。

    一方では、**テスト環境による干渉要因を排除しにくい**という問題があります。ハードウェア構成はアルゴリズムの性能に影響します。たとえば、並列度の高いアルゴリズムはマルチコア 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)と呼ばれ、略して計算量解析といいます。

    計算量解析は、アルゴリズムの実行に必要な時間資源と空間資源が入力データ規模とどのような関係にあるかを表します。これは、入力データ規模が増加するにつれて、アルゴリズムの実行に必要な時間と空間がどのように増加するかという傾向を記述するものです。この定義はややわかりにくいので、次の 3 つのポイントに分けて理解できます。

    • 「時間資源と空間資源」は、それぞれ時間計算量(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   アルゴリズムで使用される関連空間

    関連するコードを以下に示します。

    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() は実行中に未返却の recur() が同時に \\(n\\) 個存在するため、\\(O(n)\\) のスタックフレーム空間を占有します。
    ","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243","level":2,"title":"2.4.3   よくある型","text":"

    入力データサイズを \\(n\\) とすると、以下の図はよくある空間計算量の型を低い順から高い順に示しています。

    \\[ \\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
    コードの可視化

    全画面で見る >

    以下の図に示すように、この関数の再帰の深さは \\(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
    コードの可視化

    全画面で見る >

    以下の図に示すように、この関数の再帰の深さは \\(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":"

    アルゴリズム効率の評価

    • 時間効率と空間効率は、アルゴリズムの良し悪しを測る二つの主要な評価指標です。
    • 実測によってアルゴリズム効率を評価できますが、テスト環境の影響を排除しにくく、多くの計算資源も消費します。
    • 複雑度分析は実測の欠点を補い、分析結果はすべての実行プラットフォームに適用でき、データ規模ごとの効率も明らかにできます。

    時間計算量

    • 時間計算量は、アルゴリズムの実行時間がデータ量の増加に伴ってどう変化するかを測るためのものであり、効率評価に有効です。ただし、入力データ量が小さい場合や時間計算量が同じ場合などには、効率の優劣を正確に比較できないことがあります。
    • 最悪時間計算量はビッグオー記法 \\(O\\) で表され、関数の漸近上界に対応し、\\(n\\) が正の無限大に近づくときの操作回数 \\(T(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)はオブジェクトに関連付けられ、それを呼び出すオブジェクトが暗黙的に渡され、クラスのインスタンスに含まれるデータを操作できます。

    以下では、いくつかの一般的なプログラミング言語を例に説明します。

    • C 言語は手続き型プログラミング言語であり、オブジェクト指向の概念がないため、関数しかありません。ただし、構造体(struct)を作成してオブジェクト指向プログラミングを模倣でき、構造体に関連付けられた関数は、他のプログラミング言語におけるメソッドに相当します。
    • Java と C# はオブジェクト指向のプログラミング言語であり、コードブロック(メソッド)は通常あるクラスの一部です。静的メソッドの振る舞いは関数に似ており、クラスに束縛され、特定のインスタンス変数にはアクセスできません。
    • C++ と Python は、手続き型プログラミング(関数)にもオブジェクト指向プログラミング(メソッド)にも対応しています。

    Q:「一般的な空間計算量の種類」の図が表しているのは、使用空間の絶対量ですか?

    いいえ。この図が示しているのは空間計算量であり、表しているのは増加傾向であって、使用空間の絶対量ではありません。

    \\(n = 8\\) と仮定すると、各曲線の値が対応する関数と一致していないように見えるかもしれません。これは、各曲線に定数項が含まれており、値の範囲を視覚的に見やすい範囲へ圧縮しているためです。

    実際には、各手法の「定数項」の複雑度がどれほどか通常は分からないため、一般に複雑度だけを根拠に \\(n = 8\\) 以下で最適解を選ぶことはできません。ただし、\\(n = 8^5\\) であれば選びやすく、このときは増加傾向がすでに支配的になっています。

    Q 実際の利用場面に応じて、時間(または空間)を犠牲にしてアルゴリズムを設計することはありますか?

    実際の応用では、多くの場合、空間を犠牲にして時間を得る選択をします。たとえばデータベースのインデックスでは、通常 B+ 木やハッシュインデックスを構築し、大量のメモリ空間を使う代わりに、\\(O(\\log n)\\) あるいは \\(O(1)\\) の高速な検索を実現します。

    空間資源が貴重な場面では、時間を犠牲にして空間を得ることもあります。たとえば組み込み開発では、デバイスのメモリが非常に貴重なため、エンジニアはハッシュテーブルの使用をやめ、配列による順次探索を選んでメモリ使用量を節約することがあります。その代償として探索は遅くなります。

    ","path":["第 2 章   計算量解析","2.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   時間計算量","text":"

    実行時間はアルゴリズムの効率を直感的かつ正確に反映します。あるコードの実行時間を正確に見積もりたい場合、どのようにすればよいでしょうか?

    1. 実行プラットフォームを特定する。ハードウェア構成、プログラミング言語、システム環境などが含まれ、これらの要因はいずれもコードの実行効率に影響します。
    2. 各種計算操作に必要な実行時間を評価する。例えば加算 + には 1 ns 、乗算 * には 10 ns 、出力 print() には 5 ns などが必要です。
    3. コード中のすべての計算操作を数える。そして各操作の実行時間を合計することで、実行時間を得ます。

    例えば次のコードでは、入力データサイズを \\(n\\) とします:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # ある実行プラットフォーム上で\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # n 回ループ\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
    // ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // n 回ループ\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // n 回ループ\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // n 回ループ\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // n 回ループ\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // n 回ループ\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
    // ある実行プラットフォーム上で\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // n 回ループ\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // n 回ループ\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
    # ある実行プラットフォーム上で\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # n 回ループ\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

    上記の方法に基づくと、アルゴリズムの実行時間は \\((6n + 12)\\) ns になります:

    \\[ 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\\) とし、3 つのアルゴリズム ABC を考えます:

    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

    以下の図は、上記 3 つのアルゴリズム関数の時間計算量を示しています。

    • アルゴリズム A には出力操作が \\(1\\) 回しかなく、実行時間は \\(n\\) が大きくなっても増加しません。このアルゴリズムの時間計算量を「定数階」と呼びます。
    • アルゴリズム B の出力操作は \\(n\\) 回ループする必要があり、実行時間は \\(n\\) の増加に対して線形に増加します。このアルゴリズムの時間計算量は「線形階」と呼ばれます。
    • アルゴリズム C の出力操作は \\(1000000\\) 回ループする必要があり、実行時間は長いものの、入力データサイズ \\(n\\) とは無関係です。したがって C の時間計算量は A と同じく、依然として「定数階」です。

    図 2-7   アルゴリズム A、B、C の時間増加傾向

    アルゴリズムの実行時間を直接数える方法と比べて、時間計算量分析にはどのような特徴があるでしょうか?

    • 時間計算量はアルゴリズム効率を有効に評価できます。例えばアルゴリズム B の実行時間は線形に増加するため、\\(n > 1\\) ではアルゴリズム A より遅く、\\(n > 1000000\\) ではアルゴリズム C より遅くなります。実際、入力データサイズ \\(n\\) が十分に大きければ、「定数階」のアルゴリズムは必ず「線形階」のアルゴリズムより優れます。これが実行時間の増加傾向の意味です。
    • 時間計算量の見積もり方法はより簡潔です。実行プラットフォームや計算操作の種類は、アルゴリズム実行時間の増加傾向とは無関係です。そのため時間計算量分析では、すべての計算操作の実行時間を同じ「単位時間」とみなしてよく、「計算操作の実行時間を数える」作業を「計算操作の個数を数える」作業へ簡略化できます。これにより見積もりの難易度は大きく下がります。
    • 時間計算量には一定の限界もあります。例えばアルゴリズム AC の時間計算量は同じでも、実際の実行時間には大きな差があります。同様に、アルゴリズム B の時間計算量は C より高いものの、入力データサイズ \\(n\\) が小さい場合にはアルゴリズム B のほうが明らかに優れます。このような場合、時間計算量だけでアルゴリズム効率の高低を判断するのは難しいことがあります。もっとも、こうした問題があっても、複雑度分析は依然としてアルゴリズム効率を評価する最も有効で一般的な方法です。
    ","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)\\) と表します。この数学記号はビッグ \\(O\\) 記法(big-\\(O\\) notation)と呼ばれ、関数 \\(T(n)\\) の漸近上界(asymptotic upper bound)を表します。

    時間計算量の分析は本質的に「操作回数 \\(T(n)\\)」の漸近上界を求めることであり、明確な数学的定義があります。

    関数の漸近上界

    正の実数 \\(c\\) と実数 \\(n_0\\) が存在し、すべての \\(n > n_0\\) について \\(T(n) \\leq c \\cdot f(n)\\) が成り立つならば、\\(f(n)\\) は \\(T(n)\\) の漸近上界の 1 つであるとみなせます。これを \\(T(n) = O(f(n))\\) と記します。

    下図のように、漸近上界を求めるとは関数 \\(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)\\) をどのように決めればよいのでしょうか。大きく 2 段階あります。まず操作回数を数え、その後で漸近上界を判断します。

    ","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-1","level":3,"title":"1.   第 1 ステップ:操作回数を数える","text":"

    コードについては、上から下へ 1 行ずつ数えれば十分です。しかし、前述の \\(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\\) が無限大に近づくとき、最高次の項が支配的となり、他の項の影響は無視できるからです。

    以下の表はその例です。いくつか極端な値を入れているのは、「係数では次数は変わらない」という結論を強調するためです。\\(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\\) とすると、よくある時間計算量の種類は次図のとおりです(小さい順に並べています)。

    \\[ \\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\\) は入力データの型に応じて具体的に定める必要がある**ということです。例えば 1 つ目の例では変数 \\(n\\) が入力データサイズであり、2 つ目の例では配列長 \\(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
    コードの可視化

    全画面で見る >

    以下の図は、定数階・線形階・平方階の 3 種類の時間計算量を比較したものです。

    図 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":"

    生物学における「細胞分裂」は指数階増加の典型例です。初期状態では細胞が \\(1\\) 個あり、1 回分裂すると \\(2\\) 個、2 回分裂すると \\(4\\) 個となり、以下同様に、\\(n\\) 回分裂すると \\(2^n\\) 個の細胞になります。

    以下の図とコードは細胞分裂の過程を模擬したもので、時間計算量は \\(O(2^n)\\) です。なお、入力の \\(n\\) は分裂回数を表し、戻り値 count は総分裂回数を表します。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def exponential(n: int) -> int:\n    \"\"\"指数時間(ループ実装)\"\"\"\n    count = 0\n    base = 1\n    # 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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  // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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  # 細胞は各ラウンドで 2 つに分裂し、数列 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   指数階の時間計算量

    実際のアルゴリズムでも、指数階は再帰関数によく現れます。例えば次のコードでは、再帰的に 2 つへ分岐し、\\(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  # 細胞は各ラウンドで 2 つに分裂し、数列 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\\) の逆関数になります。

    以下の図とコードは、「各ラウンドで半分になる」過程を模擬したもので、時間計算量は \\(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  # 細胞は各ラウンドで 2 つに分裂し、数列 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  # 細胞は各ラウンドで 2 つに分裂し、数列 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
    コードの可視化

    全画面で見る >

    対数階は分割統治に基づくアルゴリズムによく現れ、「1 つを複数に分ける」「複雑なものを単純化する」という考え方を体現しています。増加は緩やかで、定数階に次いで理想的な時間計算量です。

    \\(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":"

    線形対数階は入れ子ループによく現れ、2 層のループの時間計算量はそれぞれ \\(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
    コードの可視化

    全画面で見る >

    下図は線形対数階がどのように生じるかを示しています。二分木の各層の操作総数はすべて \\(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 \\]

    階乗は通常、再帰で実装されます。以下の図とコードのように、第 1 層では \\(n\\) 個に分岐し、第 2 層では \\(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    # 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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  // 1個から 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    // 1個から 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    // 1個から 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  # 1個から 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":"

    アルゴリズムの時間効率は固定ではなく、入力データの分布に左右されることが多いです。長さ \\(n\\) の配列 nums を考えます。nums は \\(1\\) から \\(n\\) までの数字で構成され、各数字は 1 回だけ現れます。ただし要素の順序はランダムにシャッフルされており、目標は要素 \\(1\\) のインデックスを返すことです。ここから次の結論が得られます。

    • nums = [?, ?, ..., 1]、つまり末尾の要素が \\(1\\) の場合は、配列全体を最後まで走査する必要があり、最悪時間計算量 \\(O(n)\\) になります。
    • nums = [1, ?, ?, ...]、つまり先頭要素が \\(1\\) の場合は、配列がどれだけ長くてもそれ以上走査する必要がなく、最良時間計算量 \\(\\Omega(1)\\) になります。

    「最悪時間計算量」は関数の漸近上界に対応し、ビッグ \\(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    \"\"\"配列 nums 内で数値 1 のインデックスを探す\"\"\"\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    // システム時刻を使って乱数シードを生成する\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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### 配列 nums 内の数値 1 のインデックスを探す ###\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 章   データ構造"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"章の内容","text":"
    • 3.1   データ構造の分類
    • 3.2   基本データ型
    • 3.3   数値エンコーディング *
    • 3.4   文字エンコーディング *
    • 3.5   まとめ
    ","path":["第 3 章   データ構造"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   基本データ型","text":"

    コンピュータ内のデータについて考えるとき、テキスト、画像、動画、音声、3D モデルなど、さまざまな形態を思い浮かべます。これらのデータの構成形式はそれぞれ異なりますが、いずれも各種の基本データ型によって成り立っています。

    **基本データ型は CPU が直接演算できる型**であり、アルゴリズムの中で直接使われます。主なものは次のとおりです。

    • 整数型 byteshortintlong
    • 浮動小数点数型 floatdouble ,小数を表すために使います。
    • 文字型 char ,各言語の文字、句読点、さらには絵文字などを表すために使います。
    • 真偽値型 bool ,真か偽かの判定を表すために使います。

    基本データ型はコンピュータ内で 2 進数の形で格納されます。1 つの二進桁は \\(1\\) ビットです。現代のほとんどのオペレーティングシステムでは、\\(1\\) バイト(byte)は \\(8\\) ビット(bit)で構成されます。

    基本データ型の値域は、その型が占める領域の大きさによって決まります。以下では Java を例に取ります。

    • 整数型 byte は \\(1\\) バイト = \\(8\\) ビットを占め、\\(2^{8}\\) 個の数を表せます。
    • 整数型 int は \\(4\\) バイト = \\(32\\) ビットを占め、\\(2^{32}\\) 個の数を表せます。

    下表は、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 型はなく、1 文字は実際には長さ 1 の文字列 str です。
    • C と C++ では基本データ型の大きさは明確に規定されておらず、実装やプラットフォームによって異なります。上表は LP64 データモデル に従っており、Linux や macOS を含む Unix 系 64 ビット OS で用いられています。
    • char の大きさは C と C++ では 1 バイトですが、多くのプログラミング言語では採用する文字エンコーディング方式によって決まります。詳しくは「文字エンコーディング」の章を参照してください。
    • 真偽値を表すのに必要なのは 1 ビット(\\(0\\) または \\(1\\))だけですが、メモリ上では通常 1 バイトとして格納されます。これは、現代のコンピュータ CPU が通常 1 バイトを最小のアドレス指定可能なメモリ単位としているためです。

    では、基本データ型とデータ構造の間にはどのような関係があるのでしょうか。データ構造とは、コンピュータ内でデータを組織し格納する方法のことです。この言葉で主役なのは「データ」ではなく「構造」です。

    「数字の並び」を表したいなら、自然に配列の使用を思い浮かべるでしょう。これは、配列の線形構造が数字どうしの隣接関係や順序関係を表せるからです。しかし、格納する内容が整数 int なのか、小数 float なのか、文字 char なのかは、「データ構造」とは関係ありません。

    言い換えると、基本データ型はデータの「内容の型」を提供し、データ構造はデータの「組織方法」を提供します。たとえば次のコードでは、同じデータ構造(配列)を使って intfloatcharbool など異なる基本データ型を格納・表現しています。

    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%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%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 ビットの二進数(1 バイトの下位 7 ビット)で 1 文字を表し、最大で 128 種類の異なる文字を表現できます。下図のように、ASCII コードには英字の大文字と小文字、数字 0 ~ 9、いくつかの句読点、そしていくつかの制御文字(改行やタブなど)が含まれます。

    図 3-6   ASCII コード

    しかし、ASCII コードで表現できるのは英語だけです。コンピュータのグローバル化に伴い、より多くの言語を表せる EASCII 文字セットが生まれました。これは ASCII の 7 ビットを 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 コードでも多くの言語に必要な文字数を満たせない**ことに気づきました。たとえば漢字は 10 万字近くあり、日常的に使うものだけでも数千字あります。中国国家標準総局は 1980 年に GB2312 文字セットを公開し、6763 字の漢字を収録して、漢字のコンピュータ処理の基本的な需要を満たしました。

    しかし、GB2312 では一部の珍しい字や繁体字を扱えません。GBK 文字セットは GB2312 を基に拡張されたもので、合計 21886 字の漢字を収録しています。GBK のエンコーディング方式では、ASCII 文字は 1 バイト、漢字は 2 バイトで表されます。

    ","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode","level":2,"title":"3.4.3   Unicode 文字セット","text":"

    コンピュータ技術が急速に発展するにつれて、文字セットと符号化規格は百花繚乱の状態となり、それに伴って多くの問題も生じました。一方では、これらの文字セットは通常、特定の言語の文字しか定義しておらず、多言語環境では正常に動作できませんでした。もう一方では、同じ言語にも複数の文字セット規格が存在し、2 台のコンピュータが異なる符号化規格を使っていると、情報伝達の際に文字化けが発生しました。

    当時の研究者たちはこう考えました。十分に完全な文字セットを打ち出して、世界中のあらゆる言語と記号をそこに収録すれば、多言語環境や文字化けの問題を解決できるのではないか。この発想に後押しされて、大規模で包括的な文字セット Unicode が誕生しました。

    Unicode の中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理・表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。

    1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。巨大な Unicode 文字セットでは、よく使われる文字は 2 バイトを占め、一部の珍しい文字は 3 バイト、さらには 4 バイトを占めます。

    Unicode は汎用文字セットであり、本質的には各文字に番号(「コードポイント」)を割り当てるものですが、それらのコードポイントをコンピュータ内でどのように保存するかまでは規定していません。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか。

    この問題に対して、**すべての文字を固定長の符号として保存する**という直接的な解決策があります。下図のように、「Hello」の各文字は 1 バイト、「アルゴリズム」の各文字は 2 バイトを占めます。上位ビットを 0 で埋めることで、「Hello アルゴリズム」のすべての文字を 2 バイト長にエンコードできます。こうすれば、システムは 2 バイトごとに 1 文字を解析して、この語句の内容を復元できます。

    図 3-7   Unicode エンコーディングの例

    しかし ASCII コードはすでに、英語の符号化には 1 バイトで十分であることを示しています。上記の方式を採用すると、英語のテキストが占める空間は ASCII エンコーディング時の 2 倍になり、メモリ空間の浪費が大きくなります。そのため、より効率的な 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 文字を 1 〜 4 バイトで表し、文字の複雑さに応じて長さが変わります。ASCII 文字は 1 バイト、ラテン文字とギリシャ文字は 2 バイト、一般的な漢字は 3 バイト、そのほかの一部の珍しい文字は 4 バイト必要です。

    UTF-8 の符号化規則はそれほど複雑ではなく、次の 2 つのケースに分けられます。

    • 長さ 1 バイトの文字では、最上位ビットを \\(0\\) にし、残りの 7 ビットを Unicode コードポイントに設定します。ここで注意すべきなのは、ASCII 文字が Unicode 文字セットの先頭 128 個のコードポイントを占めていることです。つまり、UTF-8 エンコーディングは ASCII コードと下位互換性があります。このため、UTF-8 を使って古い ASCII コードのテキストを解析できます。
    • 長さ \\(n\\) バイトの文字(ただし \\(n > 1\\))では、先頭バイトの上位 \\(n\\) ビットをすべて \\(1\\) にし、第 \\(n + 1\\) ビットを \\(0\\) に設定します。2 バイト目以降では、各バイトの上位 2 ビットをいずれも \\(10\\) にし、残りのすべてのビットで文字の Unicode コードポイントを埋めます。

    下図は「Helloアルゴリズム」に対応する UTF-8 エンコーディングを示しています。観察すると、上位 \\(n\\) ビットがすべて \\(1\\) に設定されているため、システムは先頭から連続する \\(1\\) の個数を読むことで、その文字の長さが \\(n\\) であると解析できます。

    では、なぜ残りのすべてのバイトの上位 2 ビットを \\(10\\) にするのでしょうか。実は、この \\(10\\) は検査用の印として機能します。システムが誤ったバイト位置からテキストを解析し始めたとしても、バイト先頭の \\(10\\) によって異常を素早く判定できます。

    この \\(10\\) を検査用の印とする理由は、UTF-8 の符号化規則では上位 2 ビットが \\(10\\) になる文字は存在しないからです。この結論は背理法で証明できます。ある文字の上位 2 ビットが \\(10\\) だと仮定すると、その文字の長さは \\(1\\) であり、ASCII コードに対応することになります。しかし ASCII コードの最上位ビットは \\(0\\) であるはずなので、仮定と矛盾します。

    図 3-8   UTF-8 エンコーディングの例

    UTF-8 以外にも、一般的なエンコーディング方式として次の 2 つがあります。

    • UTF-16 エンコーディング:1 文字を 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\\) 文字を見つけるには文字列の先頭から第 \\(i\\) 文字まで走査する必要があり、\\(O(n)\\) の時間がかかります。
    • 文字数の計算:ランダムアクセスと同様に、UTF-16 で符号化された文字列の長さを計算するのも \\(O(1)\\) の操作です。しかし、UTF-8 で符号化された文字列の長さを計算するには、文字列全体を走査する必要があります。
    • 文字列操作:UTF-16 で符号化された文字列では、多くの文字列操作(分割、連結、挿入、削除など)をより簡単に行えます。UTF-8 で符号化された文字列では、これらの操作を行う際に、無効な UTF-8 エンコーディングを生じさせないための追加計算が通常必要になります。

    実際、プログラミング言語における文字エンコーディング方式の設計は、とても興味深い話題であり、多くの要因が関わっています。

    • Java の String 型は UTF-16 エンコーディングを使用し、各文字は 2 バイトを占めます。これは Java 言語の設計当初、人々が 16 ビットあればあらゆる文字を表現するのに十分だと考えていたためです。しかし、これは誤った判断でした。その後 Unicode 規格は 16 ビットを超える範囲へ拡張されたため、現在の Java では 1 文字が 16 ビット値の組(「サロゲートペア」)で表されることがあります。
    • JavaScript と TypeScript の文字列が UTF-16 エンコーディングを使う理由も Java と似ています。1995 年に Netscape 社が初めて JavaScript 言語を公開した当時、Unicode はまだ発展初期にあり、16 ビットの符号化で十分すべての Unicode 文字を表せると考えられていました。
    • C# が UTF-16 エンコーディングを使う主な理由は、.NET プラットフォームが Microsoft によって設計され、Microsoft の多くの技術(Windows オペレーティングシステムを含む)で UTF-16 エンコーディングが広く使われているためです。

    以上のプログラミング言語は文字数を過小評価していたため、16 ビットを超える長さの Unicode 文字を表すために「サロゲートペア」を採用せざるを得ませんでした。これはやむを得ない妥協策です。一方では、サロゲートペアを含む文字列では、1 文字が 2 バイトまたは 4 バイトを占める可能性があり、固定長エンコーディングの利点が失われます。もう一方では、サロゲートペアの処理には追加のコードが必要となり、プログラミングの複雑さとデバッグの難しさが増します。

    こうした理由から、一部のプログラミング言語では別のエンコーディング方式が採用されました。

    • Python の str は Unicode エンコーディングを使用し、柔軟な文字列表現を採用しています。保存される文字の長さは、その文字列中で最大の Unicode コードポイントに依存します。文字列がすべて ASCII 文字であれば各文字は 1 バイト、ASCII の範囲を超える文字があってもすべてが基本多言語面(BMP)内であれば各文字は 2 バイト、BMP を超える文字があれば各文字は 4 バイトを占めます。
    • Go 言語の string 型は内部で UTF-8 エンコーディングを使用します。Go 言語には単一の Unicode コードポイントを表す rune 型も用意されています。
    • Rust 言語の strString 型は内部で UTF-8 エンコーディングを使用します。Rust にも単一の Unicode コードポイントを表す char 型があります。

    注意すべきなのは、ここまでの議論はすべて、プログラミング言語内での文字列の保存方法についてであり、**文字列をファイルに保存したりネットワークで転送したりする方法とは別の問題である**ということです。ファイル保存やネットワーク転送では、通常、互換性と空間効率を最適化するために文字列を UTF-8 形式にエンコードします。

    ","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   データ構造の分類","text":"

    代表的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフがあり、これらは「論理構造」と「物理構造」の 2 つの観点から分類できます。

    ","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311","level":2,"title":"3.1.1   論理構造:線形と非線形","text":"

    論理構造はデータ要素間の論理的な関係を示します。配列と連結リストでは、データは一定の順序で並び、データ間の線形関係を表します。一方、木ではデータは上から下へ階層的に並び、「祖先」と「子孫」の派生関係を示します。グラフはノードと辺で構成され、複雑なネットワーク関係を反映します。

    以下の図に示すように、論理構造は「線形」と「非線形」の 2 つに大別できます。線形構造は比較的直感的で、データが論理関係において線形に並ぶことを指します。非線形構造はその逆で、非線形に配置されます。

    • 線形データ構造:配列、連結リスト、スタック、キュー、ハッシュテーブルであり、要素間は 1 対 1 の順序関係です。
    • 非線形データ構造:木、ヒープ、グラフ、ハッシュテーブル。

    非線形データ構造は、さらに木構造と網状構造に分けられます。

    • 木構造:木、ヒープ、ハッシュテーブルであり、要素間は 1 対多の関係です。
    • 網状構造:グラフであり、要素間は多対多の関係です。

    図 3-1   線形データ構造と非線形データ構造

    ","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312","level":2,"title":"3.1.2   物理構造:連続と分散","text":"

    アルゴリズムのプログラムが実行されるとき、処理中のデータは主にメモリに格納されます。下図はコンピュータのメモリモジュールを示しており、各黒い四角はそれぞれ 1 つのメモリ空間を表しています。メモリは巨大な Excel の表のようなものだと考えることができ、各セルには一定量のデータを格納できます。

    システムはメモリアドレスを通じて目的の位置にあるデータへアクセスします。下図に示すように、コンピュータは特定の規則に従って表内の各セルに番号を割り当て、各メモリ空間が一意のメモリアドレスを持つようにします。これらのアドレスがあれば、プログラムはメモリ内のデータにアクセスできます。

    図 3-2   メモリモジュール、メモリ空間、メモリアドレス

    Tip

    補足すると、メモリを Excel の表にたとえるのは単純化した比喩であり、実際のメモリの動作機構はより複雑で、アドレス空間、メモリ管理、キャッシュ機構、仮想メモリ、物理メモリなどの概念が関わります。

    メモリはすべてのプログラムで共有される資源であり、あるメモリ領域が 1 つのプログラムに占有されると、通常は他のプログラムが同時に利用できません。したがって、データ構造とアルゴリズムの設計では、メモリ資源は重要な考慮要素です。たとえば、アルゴリズムが使用するメモリ使用量のピークは、システムに残っている空きメモリを超えてはなりません。大きな連続メモリ領域が不足している場合、選択するデータ構造は分散したメモリ空間に格納できる必要があります。

    下図に示すように、物理構造はデータがコンピュータメモリ内にどのように格納されるかを表します。これは連続空間への格納(配列)と分散空間への格納(連結リスト)に分けられます。物理構造は低レベルでデータのアクセス、更新、追加、削除などの操作方法を決定し、2 種類の物理構造は時間効率と空間効率の面で相補的な特徴を持ちます。

    図 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-1-2","level":2,"title":"3.3.1   符号付き絶対値表現、1 の補数、2 の補数","text":"

    前節の表を見ると、すべての整数型で表せる負数の個数は正数より 1 つ多く、たとえば byte の値域は \\([-128, 127]\\) です。この現象は直感に反するように見えますが、その背景には符号付き絶対値表現、1 の補数、2 の補数に関する知識があります。

    まず押さえておくべきなのは、**数値はコンピュータ内で「2 の補数」の形で保存される**ということです。その理由を説明する前に、まずはこの 3 つの定義を示します。

    • 符号付き絶対値表現:数値の二進表現の最上位ビットを符号ビットとみなし、\\(0\\) は正数、\\(1\\) は負数を表し、残りのビットが数値の値を表します。
    • 1 の補数:正数の 1 の補数は符号付き絶対値表現と同じで、負数の 1 の補数は符号ビットを除くすべてのビットを反転したものです。
    • 2 の補数:正数の 2 の補数は符号付き絶対値表現と同じで、負数の 2 の補数は 1 の補数に \\(1\\) を加えたものです。

    下図は、符号付き絶対値表現、1 の補数、2 の補数の変換方法を示しています。

    図 3-4   符号付き絶対値表現、1 の補数、2 の補数の相互変換

    符号付き絶対値表現(sign-magnitude)は最も直感的ですが、いくつかの制約があります。まず、負数の符号付き絶対値表現はそのまま演算に使えません。たとえば符号付き絶対値表現で \\(1 + (-2)\\) を計算すると、結果は \\(-3\\) になってしまい、これは明らかに誤りです。

    \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

    この問題を解決するために、コンピュータには1 の補数(1's complement)が導入されました。まず符号付き絶対値表現を 1 の補数に変換し、1 の補数で \\(1 + (-2)\\) を計算してから、結果を 1 の補数から符号付き絶対値表現へ戻すと、正しい結果 \\(-1\\) が得られます。

    \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(符号付き絶対値表現)} + 1000 \\; 0010 \\; \\text{(符号付き絶対値表現)} \\newline & = 0000 \\; 0001 \\; \\text{(1 の補数)} + 1111 \\; 1101 \\; \\text{(1 の補数)} \\newline & = 1111 \\; 1110 \\; \\text{(1 の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(符号付き絶対値表現)} \\newline & \\rightarrow -1 \\end{aligned} \\]

    一方、数値 0 の符号付き絶対値表現には \\(+0\\) と \\(-0\\) の 2 つの表し方があります。つまり、数値 0 に対して異なる 2 つの二進コードが対応しており、これは曖昧さの原因になります。たとえば条件判定で正のゼロと負のゼロを区別しないと、誤った判定結果になる可能性があります。また、この曖昧さを解消しようとすると追加の判定処理が必要になり、計算効率が下がるおそれがあります。

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

    符号付き絶対値表現と同様に、1 の補数にも正負のゼロの曖昧さがあります。そこでコンピュータはさらに2 の補数(2's complement)を導入しました。まずは負のゼロについて、符号付き絶対値表現、1 の補数、2 の補数の変換を見てみましょう。

    \\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(符号付き絶対値表現)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(1 の補数)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(2 の補数)} \\newline \\end{aligned} \\]

    負のゼロの 1 の補数に \\(1\\) を加えると桁上がりが発生しますが、byte 型の長さは 8 ビットしかないため、第 9 ビットへあふれた \\(1\\) は捨てられます。つまり、負のゼロの 2 の補数は \\(0000 \\; 0000\\) であり、正のゼロの 2 の補数と同じです。そのため、2 の補数表現ではゼロは 1 つしか存在せず、正負のゼロの曖昧さは解消されます。

    最後にもう 1 つ疑問が残ります。byte 型の値域は \\([-128, 127]\\) ですが、余分にある負数 \\(-128\\) はどのように得られるのでしょうか。区間 \\([-127, +127]\\) にあるすべての整数には、それぞれ対応する符号付き絶対値表現、1 の補数、2 の補数があり、符号付き絶対値表現と 2 の補数の間は相互に変換できます。

    しかし、2 の補数 \\(1000 \\; 0000\\) だけは例外で、対応する符号付き絶対値表現を持ちません。変換規則に従うと、この 2 の補数に対応する符号付き絶対値表現は \\(0000 \\; 0000\\) になります。これは明らかに矛盾しています。なぜなら、この符号付き絶対値表現は数値 \\(0\\) を表し、その 2 の補数は自分自身であるはずだからです。コンピュータでは、この特別な 2 の補数 \\(1000 \\; 0000\\) を \\(-128\\) と定めています。実際、2 の補数での \\((-1) + (-127)\\) の計算結果はちょうど \\(-128\\) になります。

    \\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(符号付き絶対値表現)} + 1000 \\; 0001 \\; \\text{(符号付き絶対値表現)} \\newline & = 1000 \\; 0000 \\; \\text{(1 の補数)} + 1111 \\; 1110 \\; \\text{(1 の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(2 の補数)} + 1111 \\; 1111 \\; \\text{(2 の補数)} \\newline & = 1000 \\; 0000 \\; \\text{(2 の補数)} \\newline & \\rightarrow -128 \\end{aligned} \\]

    すでにお気づきかもしれませんが、上の計算はすべて加算です。これは重要な事実を示しています。**コンピュータ内部のハードウェア回路は、主として加算を基準に設計されている**のです。なぜなら、加算はほかの演算(乗算、除算、減算など)に比べてハードウェアで実装しやすく、並列化もしやすく、演算速度も速いからです。

    ただし、これはコンピュータが加算しかできないという意味ではありません。加算といくつかの基本的な論理演算を組み合わせることで、コンピュータはさまざまな数学演算を実現できます。たとえば減算 \\(a - b\\) は加算 \\(a + (-b)\\) に変換できますし、乗算や除算も繰り返しの加算または減算に変換できます。

    これで、コンピュータが 2 の補数を使う理由をまとめられます。2 の補数表現に基づけば、コンピュータは同じ回路と操作で正数と負数の加算を扱うことができ、減算専用の特別なハードウェア回路を設計する必要がなく、正負のゼロの曖昧さも特別に処理しなくて済みます。これにより、ハードウェア設計は大幅に簡略化され、演算効率も向上します。

    2 の補数の設計は非常に巧妙ですが、紙幅の都合上ここまでにします。興味のある読者は、さらに深く調べてみてください。

    ","path":["第 3 章   データ構造","3.3   数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332","level":2,"title":"3.3.2   浮動小数点数のエンコーディング","text":"

    注意深い人なら気づくかもしれません。intfloat はどちらも長さが 4 バイトで同じなのに、なぜ float の値域は int よりはるかに広いのでしょうか。これはかなり直感に反します。というのも、float は小数を表す必要があるので、本来なら値域は狭くなるはずだからです。

    実際には、これは浮動小数点数 float が異なる表現方法を採用しているためです。32 ビット長の二進数を次のように表します。

    \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

    IEEE 754 標準によれば、32-bit 長の float は次の 3 つの部分から構成されます。

    • 符号部 \\(\\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   IEEE 754 標準における float の計算例

    上図を見ると、例として \\(\\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 は値が大きくなるほど、隣り合う 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}\\) です。

    倍精度 doublefloat と同様の表現方法を採用しているため、ここでは詳述しません。

    ","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":"
    • データ構造は、論理構造と物理構造という 2 つの観点から分類できます。論理構造はデータ要素間の論理的関係を記述し、物理構造はデータのコンピュータメモリ上での格納方法を記述します。
    • 代表的な論理構造には、線形、木構造、網状構造などがあります。通常、論理構造に基づいてデータ構造を線形(配列、連結リスト、スタック、キュー)と非線形(木、グラフ、ヒープ)の 2 種類に分類します。ハッシュテーブルの実装には、線形データ構造と非線形データ構造が同時に含まれる場合があります。
    • プログラムの実行時、データはコンピュータメモリに格納されます。各メモリ空間には対応するメモリアドレスがあり、プログラムはそれらのメモリアドレスを通じてデータにアクセスします。
    • 物理構造は主に連続領域への格納(配列)と分散領域への格納(連結リスト)に分けられます。すべてのデータ構造は、配列、連結リスト、またはその両方の組み合わせによって実装されます。
    • コンピュータにおける基本データ型には、整数 byteshortintlong、浮動小数点数 floatdouble、文字 char、真偽値 bool があります。これらの値域は、使用する記憶領域の大きさと表現方式によって決まります。
    • 符号付き絶対値表現、1 の補数、2 の補数は、コンピュータで数値を符号化する 3 つの方法であり、相互に変換できます。整数の符号付き絶対値表現では最上位ビットが符号ビットで、残りのビットが数値の値です。
    • 整数はコンピュータ内では 2 の補数の形式で格納されます。2 の補数表現では、コンピュータは正数と負数の加算を同じように扱うことができ、減算のために特別なハードウェア回路を別途設計する必要がなく、さらに正負のゼロが重複する問題もありません。
    • 浮動小数点数の符号化は、1 ビットの符号部、8 ビットの指数部、23 ビットの仮数部で構成されます。指数部があるため、浮動小数点数の値域は整数よりはるかに広くなりますが、その代償として精度が犠牲になります。
    • ASCII コードは最も早く登場した英字文字集合で、長さは 1 バイト、収録文字数は 127 です。GBK 文字集合はよく使われる中国語文字集合で、2 万字以上の漢字を収録しています。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:なぜハッシュテーブルには線形データ構造と非線形データ構造が同時に含まれるのですか?

    ハッシュテーブルの基盤は配列であり、ハッシュ衝突を解決するために「チェイン法」(後続の「ハッシュ衝突」の章で説明します)を使うことがあります。配列内の各バケットは 1 つの連結リストを指し、その連結リストの長さがある閾値を超えると、木(通常は赤黒木)に変換されることもあります。

    格納の観点から見ると、ハッシュテーブルの基盤は配列であり、各バケットスロットには値が入ることもあれば、連結リストや木が入ることもあります。したがって、ハッシュテーブルには線形データ構造(配列、連結リスト)と非線形データ構造(木)が同時に含まれる場合があります。

    Q:char 型の長さは 1 バイトですか?

    char 型の長さは、プログラミング言語が採用する符号化方式によって決まります。たとえば、Java、JavaScript、TypeScript、C# はいずれも UTF-16 符号化(Unicode コードポイントを保持)を採用しているため、char 型の長さは 2 バイトです。

    Q:配列ベースで実装されたデータ構造を「静的データ構造」と呼ぶのは曖昧ではありませんか? スタックも push や pop などの操作ができ、これらの操作はどれも「動的」です。

    スタックは確かに動的なデータ操作を実現できますが、データ構造自体は依然として「静的」(長さが不変)です。配列ベースのデータ構造でも要素を動的に追加または削除できますが、その容量は固定です。データ量が事前に確保した大きさを超えた場合は、より大きな新しい配列を作成し、古い配列の内容を新しい配列にコピーする必要があります。

    Q:スタック(キュー)を構築するときにサイズを指定していないのに、なぜそれらは「静的データ構造」なのですか?

    高水準プログラミング言語では、スタック(キュー)の初期容量を人手で指定する必要はなく、この作業はクラス内部で自動的に行われます。たとえば、Java の ArrayList の初期容量は通常 10 です。また、容量拡張も自動的に実装されています。詳しくは後続の「リスト」の章を参照してください。

    Q:符号付き絶対値表現から 2 の補数への変換方法は「先にビット反転してから 1 を加える」ですが、2 の補数から符号付き絶対値表現への変換は逆演算である「先に 1 を引いてからビット反転する」べきなのに、同じく「先にビット反転してから 1 を加える」でも求められます。これはなぜですか?

    これは、符号付き絶対値表現と 2 の補数の相互変換が、実際には「補数」を計算する過程だからです。まず補数の定義を示します。\\(a + b = c\\) とすると、\\(a\\) を \\(b\\) から \\(c\\) への補数と呼び、逆に \\(b\\) も \\(a\\) から \\(c\\) への補数と呼びます。

    長さ \\(n = 4\\) ビットの 2 進数 \\(0010\\) が与えられたとします。この数を符号付き絶対値表現(符号ビットは考慮しない)とみなすと、その 2 の補数は「先にビット反転してから 1 を加える」ことで得られます。

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

    ここで、符号付き絶対値表現と 2 の補数の和は \\(0010 + 1110 = 10000\\) となります。つまり、2 の補数 \\(1110\\) は符号付き絶対値表現 \\(0010\\) から \\(10000\\) への「補数」です。これは、上記の「先にビット反転してから 1 を加える」が、実際には \\(10000\\) への補数を計算する過程であることを意味します。

    では、2 の補数 \\(1110\\) から \\(10000\\) への「補数」はいくつでしょうか。これもやはり「先にビット反転してから 1 を加える」ことで求められます。

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

    言い換えると、符号付き絶対値表現と 2 の補数は互いに相手から \\(10000\\) への「補数」なので、「符号付き絶対値表現から 2 の補数への変換」と「2 の補数から符号付き絶対値表現への変換」は同じ操作(先にビット反転してから 1 を加える)で実現できます。

    もちろん、逆演算を用いて 2 の補数 \\(1110\\) の符号付き絶対値表現を求めることもでき、その場合は「先に 1 を引いてからビット反転する」ことになります。

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

    まとめると、「先にビット反転してから 1 を加える」と「先に 1 を引いてからビット反転する」の 2 つの演算は、どちらも \\(10000\\) への補数を計算しており、等価です。

    本質的には、「ビット反転」という操作は実際には \\(1111\\) への補数を求めています(常に 符号付き絶対値表現 + 1 の補数 = 1111 が成り立つため)。そして、1 の補数にさらに 1 を加えて得られる 2 の補数が、\\(10000\\) への補数です。

    上記では \\(n = 4\\) を例にしましたが、この考え方は任意のビット長の 2 進数に一般化できます。

    ","path":["第 3 章   データ構造","3.5   まとめ"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"第 12 章   分割統治","text":"

    Abstract

    難題は段階的に分解され、そのたびにより単純になっていく。

    分割統治は一つの重要な事実を示している。単純なことから始めれば、すべてはもはや複雑ではない。

    ","path":["第 12 章   分割統治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"章の内容","text":"
    • 12.1   分割統治法
    • 12.2   分割統治探索戦略
    • 12.3   二分木の構築問題
    • 12.4   ハノイの塔の問題
    • 12.5   まとめ
    ","path":["第 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

    長さ \\(n\\) の昇順配列 nums が与えられ、そのすべての要素は一意である。要素 target を探索せよ。

    分割統治の観点から、探索区間 \\([i, j]\\) に対応する部分問題を \\(f(i, j)\\) と記す。

    元の問題 \\(f(0, n-1)\\) を出発点として、次の手順で二分探索を行う。

    1. 探索区間 \\([i, j]\\) の中点 \\(m\\) を計算し、それに基づいて探索区間の半分を除外する。
    2. 規模が半分に縮小された部分問題を再帰的に解く。候補は \\(f(i, m-1)\\) または \\(f(m+1, j)\\) である。
    3. 1.2. の手順を繰り返し、target が見つかるか区間が空になったら返す。

    次の図は、配列内で要素 \\(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   二分木を構築する例のデータ

    ","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1","level":3,"title":"1.   分割統治問題かどうかを判断する","text":"

    元の問題は preorderinorder から二分木を構築することであり、典型的な分割統治問題です。

    • 問題は分解できる:分割統治の観点から見ると、元の問題は 2 つの部分問題、すなわち左部分木の構築と右部分木の構築に分けられ、さらに根ノードを初期化する 1 ステップが加わります。各部分木(部分問題)に対しても、同じ分割方法を再利用してより小さな部分木(部分問題)へと分けていき、最小の部分問題(空部分木)に達した時点で終了します。
    • 部分問題は独立している:左部分木と右部分木は互いに独立しており、両者の間に重なりはありません。左部分木を構築するときは、中順走査と前順走査のうち左部分木に対応する部分だけを見れば十分です。右部分木も同様です。
    • 部分問題の解は統合できる:左部分木と右部分木(部分問題の解)が得られたら、それらを根ノードに接続することで元の問題の解を得られます。
    ","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   部分木をどのように分割するか","text":"

    以上の分析より、この問題は分割統治で解けます。では、前順走査 preorder と中順走査 inorder を使って左部分木と右部分木をどのように分割すればよいのでしょうか?

    定義に従うと、preorderinorder はいずれも 3 つの部分に分けられます。

    • 前順走査:[ 根ノード | 左部分木 | 右部分木 ] ,例えば上図の木は [ 3 | 9 | 2 1 7 ] に対応します。
    • 中順走査:[ 左部分木 | 根ノード | 右部分木 ] ,例えば上図の木は [ 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":"

    以上の分割方法により、**根ノード、左部分木、右部分木が preorderinorder の中で占めるインデックス区間**が得られました。これらのインデックス区間を表すために、いくつかのポインタ変数を導入します。

    • 現在の木の根ノードが preorder に現れるインデックスを \\(i\\) とします。
    • 現在の木の根ノードが inorder に現れるインデックスを \\(m\\) とします。
    • 現在の木が inorder において占めるインデックス区間を \\([l, r]\\) とします。

    次の表のように、これらの変数を用いれば根ノードの 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
    コードの可視化

    全画面で見る >

    下図は二分木を構築する再帰過程を示しています。各ノードは下向きに「再帰していく」過程で生成され、各辺(参照)は上向きに「戻る」過程で張られます。

    <1><2><3><4><5><6><7><8><9>

    図 12-8   二分木を構築する再帰過程

    各再帰関数における前順走査 preorder と中順走査 inorder の分割結果を下図に示します。

    図 12-9   各再帰関数での分割結果

    木のノード数を \\(n\\) とすると、各ノードの初期化(再帰関数 dfs() の 1 回の実行)には \\(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)は、問題を分けて統べるという意味であり、非常に重要で一般的なアルゴリズム戦略です。分割統治法は通常、再帰に基づいて実装され、「分」と「治」の 2 つのステップから構成されます。

    1. 分(分割段階):元の問題を 2 つ以上の部分問題へ再帰的に分解し、最小の部分問題に到達した時点で停止します。
    2. 治(統合段階):解が既知である最小の部分問題から始めて、部分問題の解を下から上へ統合し、元の問題の解を構築します。

    以下の図に示すように、「マージソート」は分割統治戦略の典型的な応用の一つです。

    1. 分:元の配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割し、部分配列に要素が 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. 部分問題の解は統合できる:元の問題の解は、部分問題の解を統合することで得られます。

    明らかに、マージソートは以上の 3 つの判断基準を満たしています。

    1. 問題は分解できる:配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割します。
    2. 部分問題は独立している:各部分配列は独立にソートできます(部分問題は独立に解けます)。
    3. 部分問題の解は統合できる:2 つの整列済み部分配列(部分問題の解)は、1 つの整列済み配列(元の問題の解)に統合できます。
    ","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212","level":2,"title":"12.1.2   分割統治法で効率を高める","text":"

    分割統治法はアルゴリズムの問題を効果的に解けるだけでなく、多くの場合アルゴリズムの効率も高められます。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートが選択ソート、バブルソート、挿入ソートより高速ですが、これは分割統治戦略を適用しているためです。

    ここで次の疑問が生じます。なぜ分割統治法はアルゴリズム効率を高められるのでしょうか。その根本的な仕組みは何でしょうか?言い換えると、大きな問題を複数の部分問題に分解し、部分問題を解き、それらの解を統合して元の問題の解にするという手順は、なぜ元の問題を直接解くより効率的なのでしょうか。この問題は、操作回数と並列計算の 2 つの観点から議論できます。

    ","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1","level":3,"title":"1.   操作回数の最適化","text":"

    「バブルソート」を例に取ると、長さ \\(n\\) の配列を処理するのに \\(O(n^2)\\) の時間がかかります。以下の図のように、配列を中央で 2 つの部分配列に分けると仮定すると、分割には \\(O(n)\\) の時間、各部分配列のソートには \\(O((n / 2)^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\\) のときに分割後の操作回数の方が少なくなり、ソート効率が高くなることを意味します。ただし、分割後の時間計算量は依然として 2 次の \\(O(n^2)\\) であり、計算量の定数項が小さくなっただけです。

    さらに考えると、部分配列を中央からさらに 2 つの部分配列へと分割し続け、部分配列に要素が 1 つだけ残るまで分割を止めないとしたらどうでしょうか。この考え方がまさに「マージソート」であり、時間計算量は \\(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":"

    一方では、分割統治法は多くの古典的なアルゴリズム問題を解くのに使えます。

    • 最近点対探索:このアルゴリズムは、まず点集合を 2 つに分け、それぞれの部分における最近点対を求め、最後に 2 つの部分をまたぐ最近点対を求めます。
    • 大整数乗算:たとえば Karatsuba 法では、大整数の乗算を、より小さな整数どうしのいくつかの乗算と加算に分解します。
    • 行列乗算:たとえば Strassen 法では、大きな行列の乗算を、複数の小さな行列の乗算と加算に分解します。
    • ハノイの塔問題:ハノイの塔問題は再帰によって解くことができ、これは典型的な分割統治戦略の応用です。
    • 反転対の計算:ある数列で前の数が後ろの数より大きい場合、その 2 つの数は反転対を構成します。反転対の問題は、分割統治の考え方を利用し、マージソートを用いて解けます。

    他方で、分割統治法はアルゴリズムとデータ構造の設計にも非常に広く応用されています。

    • 二分探索:二分探索では、整列済み配列を中央のインデックスで 2 つに分け、目標値と中央要素の比較結果に基づいてどちらの半区間を除外するかを決め、残った区間で同じ二分操作を行います。
    • マージソート:本節の冒頭で紹介したため、ここでは繰り返しません。
    • クイックソート:クイックソートは基準値を 1 つ選び、配列を、基準値より小さい要素の部分配列と、基準値より大きい要素の部分配列に分け、その後それぞれに対して同じ分割操作を行い、部分配列に要素が 1 つだけ残るまで続けます。
    • バケットソート:バケットソートの基本的な考え方は、データを複数のバケットに分散し、各バケット内の要素をソートしたうえで、各バケットの要素を順に取り出して整列済み配列を得ることです。
    • 木構造:たとえば二分探索木、AVL 木、赤黒木、B 木、B+ 木などでは、探索・挿入・削除などの操作をいずれも分割統治戦略の応用とみなせます。
    • ヒープ:ヒープは特殊な完全二分木であり、挿入、削除、ヒープ化などの各種操作には、実際には分割統治の考え方が含まれています。
    • ハッシュテーブル:ハッシュテーブル自体は分割統治を直接適用しているわけではありませんが、いくつかのハッシュ衝突解決法では間接的に分割統治戦略が使われています。たとえば、連鎖アドレス法における長い連結リストは、検索効率を高めるために赤黒木へ変換されます。

    このように、**分割統治法は「静かに物を潤す」ようなアルゴリズム思想**であり、さまざまなアルゴリズムやデータ構造の中に潜んでいます。

    ","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   ハノイの塔の問題","text":"

    マージソートや二分木の構築では、いずれも元の問題を元問題の半分の規模をもつ 2 つの部分問題に分解していました。しかし、ハノイの塔の問題では、異なる分解戦略を採用します。

    Question

    3 本の柱があり、それぞれを ABC とします。初期状態では、柱 A に \\(n\\) 枚の円盤が通されており、上から下へ小さい順に並んでいます。私たちの課題は、この \\(n\\) 枚の円盤を柱 C に移し、元の順序を保つことです(以下の図のとおり)。円盤を移動する際には、次のルールに従う必要があります。

    1. 円盤は 1 本の柱の頂上から取り出し、別の柱の頂上に置くことしかできません。
    2. 1 回に移動できる円盤は 1 枚だけです。
    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":"

    以下の図に示すように、問題 \\(f(1)\\) 、すなわち円盤が 1 枚だけの場合は、それを A から C へ直接移動すれば済みます。

    <1><2>

    図 12-11   規模 1 の問題の解

    以下の図に示すように、問題 \\(f(2)\\) 、すなわち円盤が 2 枚ある場合は、小さい円盤が常に大きい円盤の上にある条件を満たすため、B を借りて移動を行う必要があります。

    1. まず上の小さい円盤を A から B へ移します。
    2. 次に大きい円盤を A から C へ移します。
    3. 最後に小さい円盤を B から C へ移します。
    <1><2><3><4>

    図 12-12   規模 2 の問題の解

    問題 \\(f(2)\\) を解く過程は、**2 枚の円盤を B を介して A から C へ移す**と要約できます。このとき、C を目標の柱、B を補助の柱と呼びます。

    ","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2","level":3,"title":"2.   部分問題への分解","text":"

    問題 \\(f(3)\\) 、すなわち円盤が 3 枚ある場合になると、状況はやや複雑になります。

    \\(f(1)\\) と \\(f(2)\\) の解が既知なので、分割統治の観点から、A の上部にある 2 枚の円盤をひとまとまりとみなして、次の図の手順を実行できます。こうして 3 枚の円盤を A から C へ順調に移動できます。

    1. B を目標の柱、C を補助の柱として、2 枚の円盤を A から B へ移します。
    2. A に残った 1 枚の円盤を A から C へ直接移動します。
    3. C を目標の柱、A を補助の柱として、2 枚の円盤を B から C へ移します。
    <1><2><3><4>

    図 12-13   規模 3 の問題の解

    本質的には、問題 \\(f(3)\\) を 2 つの部分問題 \\(f(2)\\) と 1 つの部分問題 \\(f(1)\\) に分けています 。この 3 つの部分問題を順に解けば、元の問題も解決されます。これは、部分問題が独立しており、解を組み合わせられることを示しています。

    ここまでで、次の図に示すハノイの塔の問題を解く分割統治戦略をまとめられます。元の問題 \\(f(n)\\) を 2 つの部分問題 \\(f(n-1)\\) と 1 つの部分問題 \\(f(1)\\) に分け、次の順序でこの 3 つの部分問題を解きます。

    1. \\(n-1\\) 枚の円盤を C を介して A から B へ移します。
    2. 残り \\(1\\) 枚の円盤を A から C へ直接移します。
    3. \\(n-1\\) 枚の円盤を A を介して B から C へ移します。

    この 2 つの部分問題 \\(f(n-1)\\) は、同じ方法で再帰的に分割できます。最小の部分問題 \\(f(1)\\) に到達するまでこれを続けます。一方、\\(f(1)\\) の解は既知であり、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) を定義します。その役割は、柱 src の上部にある \\(i\\) 枚の円盤を、補助の柱 buf を使って目標の柱 tar へ移動することです:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
    def move(src: list[int], tar: list[int]):\n    \"\"\"円盤を 1 枚移動\"\"\"\n    # src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1:\n        move(src, tar)\n        return\n    # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf)\n    # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    # A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C)\n
    hanota.cpp
    /* 円盤を 1 枚移動 */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.java
    /* 円盤を 1 枚移動 */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.cs
    /* 円盤を 1 枚移動 */\nvoid Move(List<int> src, List<int> tar) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    DFS(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    Move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    DFS(n, A, B, C);\n}\n
    hanota.go
    /* 円盤を 1 枚移動 */\nfunc move(src, tar *list.List) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfsHanota(i-1, src, tar, buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* ハノイの塔を解く */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfsHanota(n, A, B, C)\n}\n
    hanota.swift
    /* 円盤を 1 枚移動 */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src: &src, tar: &tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // src の上から n 個の円盤を、B を介して C に移動する\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
    hanota.js
    /* 円盤を 1 枚移動 */\nfunction move(src, tar) {\n    // src の上から円盤を1枚取り出す\n    const pan = src.pop();\n    // 円盤を tar の上に置く\n    tar.push(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunction dfs(i, src, buf, tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.ts
    /* 円盤を 1 枚移動 */\nfunction move(src: number[], tar: number[]): void {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.dart
    /* 円盤を 1 枚移動 */\nvoid move(List<int> src, List<int> tar) {\n  // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n  dfs(i - 1, src, tar, buf);\n  // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n  move(src, tar);\n  // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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  // A の上から n 枚の円盤を B を介して C へ移す\n  dfs(n, A, B, C);\n}\n
    hanota.rs
    /* 円盤を 1 枚移動 */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move_pan(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.c
    /* 円盤を 1 枚移動 */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, srcSize, tar, tarSize);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
    hanota.kt
    /* 円盤を 1 枚移動 */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C)\n}\n
    hanota.rb
    ### 円盤を1枚移動 ###\ndef move(src, tar)\n  # src の上から円盤を1枚取り出す\n  pan = src.pop\n  # 円盤を tar の上に置く\n  tar << pan\nend\n\n### ハノイの塔 f(i) を解く ###\ndef dfs(i, src, buf, tar)\n  # src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n  dfs(i - 1, src, tar, buf)\n  # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n  move(src, tar)\n  # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n  dfs(i - 1, buf, src, tar)\nend\n\n### ハノイの塔を解く ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # A の上から n 枚の円盤を B を介して C へ移す\n  dfs(n, _A, _B, _C)\nend\n
    コードの可視化

    全画面で見る >

    以下の図に示すように、ハノイの塔の問題は高さ \\(n\\) の再帰木を形成し、各ノードは 1 つの部分問題、すなわち 1 つ起動された dfs() 関数に対応します。したがって時間計算量は \\(O(2^n)\\) 、空間計算量は \\(O(n)\\) です。

    図 12-15   ハノイの塔の問題の再帰木

    Quote

    ハノイの塔の問題は古い伝説に由来します。古代インドのある寺院で、僧侶たちは 3 本の高いダイヤモンドの柱と、\\(64\\) 枚の大きさの異なる金の円盤を持っていました。僧侶たちは絶えず円盤を動かし、最後の 1 枚が正しく置かれた瞬間に世界が終わると信じていました。

    しかし、たとえ僧侶たちが 1 秒に 1 回移動するとしても、合計でおよそ \\(2^{64} \\approx 1.84×10^{19}\\) 秒、約 \\(5850\\) 億年が必要で、現在推定されている宇宙の年齢をはるかに上回ります。したがって、この伝説が本当だったとしても、世界の終わりを心配する必要はなさそうです。

    ","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":"
    • 分割統治法は一般的なアルゴリズム設計戦略であり、分(分割)と治(統合)の 2 つの段階からなり、通常は再帰に基づいて実装されます。
    • それが分割統治法の問題かどうかを判断する基準には、問題を分解できるか、部分問題が独立しているか、部分問題を統合できるかが含まれます。
    • マージソートは分割統治法の典型的な応用であり、配列を再帰的に同じ長さの 2 つの部分配列に分割し、要素が 1 つだけになるまで続け、その後で各層を順に統合してソートを完了します。
    • 分割統治法を導入すると、多くの場合アルゴリズムの効率を高められます。一方では操作回数が減り、他方では分割後にシステムの並列最適化を行いやすくなります。
    • 分割統治法は多くのアルゴリズム問題を解決できるだけでなく、データ構造やアルゴリズム設計にも広く応用されており、至る所でその姿を見ることができます。
    • 総当たり探索と比べて、適応的な探索のほうが効率的です。時間計算量が \\(O(\\log n)\\) の探索アルゴリズムは、通常は分割統治法に基づいて実装されます。
    • 二分探索は分割統治法のもう 1 つの典型的な応用であり、部分問題の解を統合する手順を含みません。再帰的な分割統治によって二分探索を実現できます。
    • 二分木を構築する問題では、木の構築(元の問題)を左部分木と右部分木の構築(部分問題)に分けられます。これは、先行順序走査と中間順序走査のインデックス区間を分割することで実現できます。
    • ハノイの塔の問題では、規模 \\(n\\) の問題を、規模 \\(n-1\\) の 2 つの部分問題と規模 \\(1\\) の 1 つの部分問題に分けられます。これら 3 つの部分問題を順に解くと、元の問題も解決されます。
    ","path":["第 12 章   分割統治","12.5   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"第 14 章   動的計画法","text":"

    Abstract

    小川は川へと注ぎ、河川は大海へと注ぐ。

    動的計画法は小さな問題の解を集めて大きな問題の答えとし、一歩ずつ私たちを問題解決の彼岸へと導く。

    ","path":["第 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 章   動的計画法"],"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]\\) は地面(開始地点)です。頂上に到達するために必要な最小コストを求めてください。

    下図に示すように、第 \\(1\\)、\\(2\\)、\\(3\\) 段のコストがそれぞれ \\(1\\)、\\(10\\)、\\(1\\) である場合、地面から第 \\(3\\) 段まで上る最小コストは \\(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   階段昇り最小コストの動的計画法の過程

    この問題では空間最適化も可能であり、一次元をゼロ次元まで圧縮することで、空間計算量を \\(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\\) 段上ることができます。ただし、連続する 2 回で \\(1\\) 段ずつ上ることはできません。頂上まで上る方法は何通りあるでしょうか。

    下図に示すように、第 \\(3\\) 段まで上る実行可能な方法は \\(2\\) 通りしか残りません。そのうち、\\(1\\) 段ずつ 3 回連続で上る方法は制約を満たさないため除外されます。

    図 14-8   制約付きで第 3 段まで上る方法数

    この問題では、前回が \\(1\\) 段上りだった場合、次回は必ず \\(2\\) 段上らなければなりません。これは、**次の一手が現在の状態(現在いる階段の段数)だけでは独立に決まらず、一つ前の状態(前回いた段数)にも関係する**ことを意味します。

    容易に分かるように、この問題はもはや無後効性を満たしておらず、状態遷移方程式 \\(dp[i] = dp[i-1] + dp[i-2]\\) も成立しません。というのも、\\(dp[i-1]\\) は今回 \\(1\\) 段上る場合を表しますが、その中には「前回も \\(1\\) 段上ってきた」方法が多数含まれており、制約を満たすためには \\(dp[i-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]\\) から遷移できます。

    下図に示すように、この定義のもとでは \\(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 回でそれぞれ第 \\(2\\) 段、第 \\(3\\) 段に到達した場合、その後は第 \\(4\\) 段と第 \\(6\\) 段に跳ぶことはできません。頂上まで上る方法は何通りあるでしょうか。

    この問題では、次の跳躍が過去のすべての状態に依存します。なぜなら、各跳躍がより高い段に障害物を設置し、将来の跳躍に影響するからです。この種の問題は、動的計画法では解きにくいことが多いです。

    実際、多くの複雑な組合せ最適化問題(例えば巡回セールスマン問題)は無後効性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を用いて、限られた時間内に実用的な局所最適解を得ます。

    ","path":["第 14 章   動的計画法","14.2   動的計画法の問題特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   動的計画法の問題解決の考え方","text":"

    前の 2 節では動的計画法の問題の主要な特徴を紹介しました。ここからは、さらに実用的な 2 つの問題を一緒に考えていきます。

    1. ある問題が動的計画法の問題かどうかを、どのように判断すればよいでしょうか?
    2. 動的計画法の問題を解くには、どこから着手し、完全な手順はどのようなものでしょうか?
    ","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431","level":2,"title":"14.3.1   問題の判定","text":"

    一般に、ある問題が重複部分問題と最適部分構造を含み、さらに無後效性を満たしているなら、通常は動的計画法で解くのに適しています。しかし、問題文からこれらの性質を直接読み取るのは簡単ではありません。そのため通常は条件を少し緩めて、**まずその問題がバックトラッキング(全探索)で解くのに適しているか**を観察します。

    バックトラッキングで解くのに適した問題は、通常「決定木モデル」を満たします。この種の問題は木構造で表現でき、各ノードは 1 つの決定を表し、各経路は 1 つの決定列を表します。

    言い換えると、問題に明確な決定の概念が含まれており、解が一連の決定によって生成されるなら、その問題は決定木モデルを満たし、通常はバックトラッキングで解くことができます。

    これに加えて、動的計画法の問題には判定のための「加点要素」もあります。

    • 問題文に最大(最小)や最多(最少)などの最適化に関する記述がある。
    • 問題の状態が配列、多次元行列、または木で表現でき、ある状態とその周辺の状態の間に漸化的な関係がある。

    反対に、「減点要素」もあります。

    • 問題の目的が最適解を求めることではなく、あり得るすべての解を列挙することである。
    • 問題文に明確な順列・組合せの特徴があり、具体的な複数の解を返す必要がある。

    ある問題が決定木モデルを満たし、さらに比較的明確な「加点要素」を備えているなら、その問題は動的計画法の問題であると仮定し、解く過程でそれを検証できます。

    ","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432","level":2,"title":"14.3.2   問題を解く手順","text":"

    動的計画法の解法の流れは問題の性質や難易度によって異なりますが、通常は次の手順に従います。すなわち、決定を記述し、状態を定義し、\\(dp\\) テーブルを構築し、状態遷移方程式を導出し、境界条件を定めます。

    解法の手順をより具体的に示すために、ここでは古典的な問題である「最小経路和」を例にします。

    Question

    \\(n \\times m\\) の 2 次元グリッド grid が与えられます。グリッドの各セルには非負整数が格納されており、そのセルのコストを表します。ロボットは左上のセルを始点とし、毎回下または右に 1 マスだけ移動して、右下のセルまで進みます。左上から右下までの最小経路和を返してください。

    次の図は 1 つの例を示しており、このグリッドの最小経路和は \\(13\\) です。

    図 14-10   最小経路和のサンプルデータ

    ステップ 1:各ラウンドの決定を考え、状態を定義して、\\(dp\\) テーブルを得る

    この問題における各ラウンドの決定は、現在のマスから下または右へ 1 マス進むことです。現在のマスの行・列インデックスを \\([i, j]\\) とすると、下または右へ 1 マス進んだ後のインデックスは \\([i+1, j]\\) または \\([i, j+1]\\) になります。したがって、状態には行インデックスと列インデックスの 2 つの変数を含め、\\([i, j]\\) と表します。

    状態 \\([i, j]\\) に対応する部分問題は、始点 \\([0, 0]\\) から \\([i, j]\\) まで進む最小経路和であり、その解を \\(dp[i, j]\\) と記します。

    これで、次の図に示す 2 次元の \\(dp\\) 行列が得られます。そのサイズは入力グリッド \\(grid\\) と同じです。

    図 14-11   状態の定義と dp テーブル

    Note

    動的計画法とバックトラッキングの過程は、いずれも 1 つの決定列として記述できます。そして状態は、すべての決定変数から構成されます。状態には解法の進行状況を表すすべての変数が含まれているべきであり、次の状態を導くのに十分な情報を持っている必要があります。

    各状態は 1 つの部分問題に対応しており、すべての部分問題の解を保存するために \\(dp\\) テーブルを定義します。状態の各独立変数は、\\(dp\\) テーブルの 1 つの次元に対応します。本質的に、\\(dp\\) テーブルは状態と部分問題の解との対応関係です。

    ステップ 2:最適部分構造を見つけ、状態遷移方程式を導出する

    状態 \\([i, j]\\) は、上のマス \\([i-1, j]\\) または左のマス \\([i, j-1]\\) からしか遷移してきません。したがって最適部分構造は、\\([i, j]\\) に到達する最小経路和が、\\([i, j-1]\\) の最小経路和と \\([i-1, j]\\) の最小経路和のうち小さい方によって決まる、ということです。

    以上の分析から、次の図に示す状態遷移方程式を導くことができます。

    \\[ 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   境界条件と状態遷移の順序

    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]\\) 。
    • 戻り値:\\([0, 0]\\) から \\([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
    コードの可視化

    全画面で見る >

    次の図は、\\(dp[2, 1]\\) を根ノードとする再帰木を示しています。この中にはいくつかの重複部分問題が含まれており、その数はグリッド grid のサイズが大きくなるにつれて急激に増加します。

    本質的に、重複部分問題が生じる理由は、**左上からあるセルへ到達する経路が複数存在すること**にあります。

    図 14-14   力任せ探索の再帰木

    各状態には下と右の 2 通りの選択肢があり、左上から右下まで進むには合計で \\(m + n - 2\\) 歩必要です。したがって最悪時間計算量は \\(O(2^{m + n})\\) です。ここで、\\(n\\) と \\(m\\) はそれぞれグリッドの行数と列数を表します。なお、この見積もりではグリッド境界付近の状況を考慮していません。境界に達すると選択肢は 1 つだけになるため、実際の経路数はこれより少なくなります。

    ","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-2","level":3,"title":"2.   方法 2:メモ化探索","text":"

    グリッド grid と同じサイズのメモ配列 mem を導入し、各部分問題の解を記録して、重複部分問題を枝刈りします。

    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
    コードの可視化

    全画面で見る >

    次の図に示すように、メモ化を導入すると、すべての部分問題の解は 1 回だけ計算すればよくなります。したがって時間計算量は状態総数、すなわちグリッドサイズの \\(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
    コードの可視化

    全画面で見る >

    次の図は最小経路和の状態遷移の過程を示しています。グリッド全体を走査するため、時間計算量は \\(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":"

    各マスは左のマスと上のマスにのみ関係するため、1 行の配列だけを使って \\(dp\\) テーブルを実装できます。

    ただし、配列 dp は 1 行分の状態しか表せないため、先頭列の状態を事前に初期化することはできず、各行を走査するときに更新する必要があります。

    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":"

    編集距離は、Levenshtein 距離とも呼ばれ、2つの文字列の相互変換に必要な最小の編集回数を指し、通常は情報検索や自然言語処理において2つの系列の類似度を測るために用いられます。

    Question

    2つの文字列 \\(s\\) と \\(t\\) を入力し、\\(s\\) を \\(t\\) に変換するのに必要な最小編集回数を返してください。

    1つの文字列に対して3種類の編集操作を行えます。1文字の挿入、1文字の削除、任意の文字への置換です。

    下図に示すように、kittensitting に変換するには 3 回の編集が必要で、内訳は 2 回の置換と 1 回の挿入です。helloalgo に変換する場合も 3 回必要で、内訳は 2 回の置換と 1 回の削除です。

    図 14-27   編集距離のサンプルデータ

    編集距離問題は決定木モデルで自然に説明できます。文字列が木のノードに対応し、1回の決定(1回の編集操作)が木の1本の辺に対応します。

    下図に示すように、操作に制限がない場合、各ノードからは多くの辺を派生でき、それぞれの辺が1種類の操作に対応します。これは 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\\) に対して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\\) に対して1回の編集(挿入、削除、置換)を行い、両文字列の末尾の文字を同じにします。そうすることでそれらをスキップし、より小さい問題を考えられます。

    つまり、文字列 \\(s\\) に対する各ラウンドの決定(編集操作)は、\\(s\\) と \\(t\\) における残りの未一致文字を変化させます。したがって、状態は現在 \\(s\\) と \\(t\\) で考えている第 \\(i\\) と第 \\(j\\) 文字とし、\\([i, j]\\) と記します。

    状態 \\([i, j]\\) に対応する部分問題は、**\\(s\\) の先頭 \\(i\\) 文字を \\(t\\) の先頭 \\(j\\) 文字に変換するのに必要な最小編集回数**です。

    これにより、サイズが \\((i+1) \\times (j+1)\\) の2次元 \\(dp\\) テーブルが得られます。

    第2ステップ:最適部分構造を見つけ、状態遷移方程式を導く

    部分問題 \\(dp[i, j]\\) を考えます。これに対応する2つの文字列の末尾文字は \\(s[i-1]\\) と \\(t[j-1]\\) であり、編集操作の違いに応じて下図の3つの場合に分けられます。

    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]\\) の3つのうち最小の編集回数に、今回の編集回数 \\(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ステップ:境界条件と状態遷移の順序を決める

    2つの文字列がともに空のとき、編集回数は \\(0\\)、すなわち \\(dp[0, 0] = 0\\) です。\\(s\\) が空で \\(t\\) が空でないとき、最小編集回数は \\(t\\) の長さに等しいため、先頭行は \\(dp[0, j] = j\\) です。\\(s\\) が空でなく \\(t\\) が空のとき、最小編集回数は \\(s\\) の長さに等しいため、先頭列は \\(dp[i, 0] = i\\) です。

    状態遷移方程式を観察すると、\\(dp[i, j]\\) の解は左、上、左上の解に依存します。そのため、2重ループで \\(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                # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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        // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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        # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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
    コードの可視化

    全画面で見る >

    下図に示すように、編集距離問題の状態遷移の過程はナップサック問題と非常によく似ており、どちらも2次元グリッドを埋めていく過程とみなせます。

    <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-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                # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            else:\n                # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftUp\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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        // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[j] = leftup;\n      } else {\n        // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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        # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[j] = leftup\n      else\n        # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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\\) 段上ることができます。頂上まで到達する方法は何通りあるでしょうか?

    次の図に示すように、\\(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 -1\\) 段目または \\(i - 2\\) 段目からしか \\(i\\) 段目へ進めません。

    ここから重要な帰結が得られます。**\\(i - 1\\) 段目まで上る方法数と \\(i - 2\\) 段目まで上る方法数の和が、\\(i\\) 段目まで上る方法数に等しい**のです。式は次のとおりです:

    \\[ dp[i] = dp[i-1] + dp[i-2] \\]

    これは、階段を上る問題では各部分問題の間に漸化関係があり、**元の問題の解は部分問題の解から構築できる**ことを意味します。次の図はこの漸化関係を示しています。

    図 14-2   方法数の漸化関係

    漸化式に基づいて総当たり探索の解法を得ることができます。\\(dp[n]\\) を出発点とし、**より大きな問題を再帰的に 2 つのより小さな問題の和へ分解**していき、最小部分問題 \\(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
    コードの可視化

    全画面で見る >

    次の図は総当たり探索によって形成される再帰木を示しています。問題 \\(dp[n]\\) に対して、その再帰木の深さは \\(n\\)、時間計算量は \\(O(2^n)\\) です。指数オーダーは爆発的に増加するため、比較的大きな \\(n\\) を入力すると長時間待たされることになります。

    図 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":"

    アルゴリズム効率を高めるため、**すべての重複部分問題を 1 回だけ計算したい**と考えます。そのために、各部分問題の解を記録する配列 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
    コードの可視化

    全画面で見る >

    次の図を見ると、メモ化を行うことで、すべての重複部分問題は 1 回だけ計算すればよくなり、時間計算量は \\(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   階段上りの動的計画法の過程

    バックトラッキング法と同様に、動的計画法でも問題解決の特定段階を表すために「状態」という概念を用います。各状態は 1 つの部分問題と、それに対応する局所最適解に対応します。たとえば、階段を上る問題では、状態は現在いる階段の段数 \\(i\\) と定義されます。

    以上を踏まえると、動的計画法のよく使われる用語を次のようにまとめられます。

    • 配列 dp を dp テーブル と呼び、\\(dp[i]\\) は状態 \\(i\\) に対応する部分問題の解を表します。
    • 最小部分問題に対応する状態(第 \\(1\\) 段目と第 \\(2\\) 段目の階段)を初期状態と呼びます。
    • 漸化式 \\(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 を使う必要はありません。2 つの変数を順に更新していくだけで十分です。コードは次のとおりです:

    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":"

    ナップサック問題は、動的計画法の入門として非常に適した問題であり、動的計画法で最もよく見られる問題形式の1つです。これには 0-1 ナップサック問題、完全ナップサック問題、多重ナップサック問題など、多くの派生があります。

    本節では、まず最も一般的な 0-1 ナップサック問題を解いていきます。

    Question

    \\(n\\) 個の品物が与えられ、\\(i\\) 番目の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量 \\(cap\\) のナップサックがあります。各品物は1回しか選べないとき、ナップサック容量の制約下で入れられる品物の最大価値を求めてください。

    以下の図を見てみましょう。品物番号 \\(i\\) は \\(1\\) から始まり、配列のインデックスは \\(0\\) から始まるため、品物 \\(i\\) は重さ \\(wgt[i-1]\\)、価値 \\(val[i-1]\\) に対応します。

    図 14-17   0-1 ナップサックのサンプルデータ

    0-1 ナップサック問題は、\\(n\\) 回の意思決定からなる過程とみなせます。各品物について「入れない」「入れる」という2つの選択肢があるため、この問題は決定木モデルを満たします。

    この問題の目的は「ナップサック容量の制約下で入れられる品物の最大価値」を求めることなので、動的計画法の問題である可能性が高いです。

    ステップ1:各ラウンドの選択を考え、状態を定義して、\\(dp\\) テーブルを得る

    各品物について、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。ここから状態を、現在の品物番号 \\(i\\) とナップサック容量 \\(c\\) として定義し、\\([i, c]\\) と表せます。

    状態 \\([i, c]\\) に対応する部分問題は、先頭 \\(i\\) 個の品物を容量 \\(c\\) のナップサックに入れるときの最大価値 であり、これを \\(dp[i, c]\\) と記します。

    求めるべきものは \\(dp[n, cap]\\) なので、サイズ \\((n+1) \\times (cap+1)\\) の2次元 \\(dp\\) テーブルが必要です。

    ステップ2:最適部分構造を見つけ、状態遷移方程式を導く

    品物 \\(i\\) に対する選択を行った後に残るのは、先頭 \\(i-1\\) 個の品物に対する部分問題であり、次の2つのケースに分けられます。

    • 品物 \\(i\\) を入れない :ナップサック容量は変わらず、状態は \\([i-1, c]\\) に変化します。
    • 品物 \\(i\\) を入れる :ナップサック容量は \\(wgt[i-1]\\) だけ減少し、価値は \\(val[i-1]\\) だけ増加して、状態は \\([i-1, c-wgt[i-1]]\\) に変化します。

    以上の分析から、この問題の最適部分構造が分かります。すなわち、最大価値 \\(dp[i, c]\\) は、品物 \\(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]\\) はいずれも \\(0\\) になります。

    現在の状態 \\([i, c]\\) は、上側の状態 \\([i-1, c]\\) と左上の状態 \\([i-1, c-wgt[i-1]]\\) から遷移してくるため、2重ループで \\(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    # 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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  // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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  # 2つの案のうち価値が大きいほうを返す\n  [no, yes].max\nend\n
    コードの可視化

    全画面で見る >

    以下の図のように、各品物ごとに「選ばない」「選ぶ」の2つの探索分岐が生じるため、時間計算量は \\(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    # 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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  // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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  # 2 つの案のうち価値が大きい方を記録して返す\n  mem[i][c] = [no, yes].max\nend\n
    コードの可視化

    全画面で見る >

    次の図は、メモ化探索で剪定された探索分岐を示しています。

    図 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
    コードの可視化

    全画面で見る >

    以下の図のように、時間計算量と空間計算量はいずれも配列 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":"

    各状態は直前の行の状態にしか依存しないため、2つの配列をローテーションして用いることで、空間計算量を \\(O(n^2)\\) から \\(O(n)\\) に削減できます。

    さらに考えると、1つの配列だけで空間最適化を実現できるでしょうか。観察すると、各状態は真上または左上のマスから遷移してきます。配列が1つしかないと仮定すると、\\(i\\) 行目の走査を開始した時点では、その配列にはまだ \\(i-1\\) 行目の状態が格納されています。

    • 順方向に走査すると、\\(dp[i, j]\\) に到達した時点で、左上にある \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) の値がすでに上書きされている可能性があり、正しい状態遷移結果を得られません。
    • 逆方向に走査すれば、上書きの問題は発生せず、状態遷移を正しく行えます。

    次の図は、単一配列のもとで \\(i = 1\\) 行目から \\(i = 2\\) 行目へ変換する過程を示しています。順方向走査と逆方向走査の違いを考えてみてください。

    <1><2><3><4><5><6>

    図 14-21   0-1 ナップサックの空間最適化後の動的計画法の過程

    コード実装では、配列 dp の第1次元 \\(i\\) をそのまま削除し、内側のループを逆方向走査に変更するだけで済みます。

    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":"
    • 動的計画法は問題を分解し、部分問題の解を保存することで重複計算を避け、計算効率を高めます。
    • 時間を考慮しなければ、すべての動的計画法の問題はバックトラッキング(総当たり探索)で解けますが、再帰木には大量の重複部分問題が存在するため、効率はきわめて低くなります。メモ化配列を導入すると、計算済みのすべての部分問題の解を保存でき、重複部分問題が 1 回だけ計算されることを保証できます。
    • メモ化探索はトップダウンの再帰的解法であり、それに対応する動的計画法はボトムアップの漸化式による解法で、ちょうど「表を埋める」ようなものです。現在の状態は一部の局所状態にのみ依存するため、\\(dp\\) 表の 1 次元を削減して空間計算量を下げることができます。
    • 部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治、動的計画法、バックトラッキングではそれぞれ異なる性質を持ちます。
    • 動的計画法の問題には 3 つの大きな特徴があります。重複部分問題、最適部分構造、無後効性です。
    • 元の問題の最適解が部分問題の最適解から構築できるなら、その問題は最適部分構造を持ちます。
    • 無後効性とは、ある状態の将来の発展がその状態のみに関係し、過去に経たすべての状態とは無関係であることを指します。多くの組合せ最適化問題は無後効性を持たず、動的計画法で高速に解くことはできません。

    ナップサック問題

    • ナップサック問題は最も典型的な動的計画法の問題の 1 つであり、0-1 ナップサック、完全ナップサック、多重ナップサックなどの派生があります。
    • 0-1 ナップサックの状態は、容量 \\(c\\) のナップサックに対して、前 \\(i\\) 個の品物で得られる最大価値として定義されます。ナップサックに入れない場合と入れる場合の 2 つの判断から最適部分構造を得て、状態遷移方程式を構築できます。空間最適化では、各状態が真上と左上の状態に依存するため、左上の状態が上書きされるのを避けるために配列を逆順に走査する必要があります。
    • 完全ナップサック問題では各品物の選択数に制限がないため、品物を入れる場合の状態遷移は 0-1 ナップサック問題とは異なります。状態は真上と真左の状態に依存するので、空間最適化では順方向に走査するべきです。
    • コイン両替問題は完全ナップサック問題の変種です。「最大」価値を求める問題から「最小」の硬貨枚数を求める問題へ変わるため、状態遷移方程式の \\(\\max()\\) は \\(\\min()\\) に置き換える必要があります。また、ナップサック容量を「超えない」ことを目指すのではなく、目標金額を「ちょうど」作ることを目指すため、\\(amt + 1\\) を「目標金額を作れない」無効解の表現として用います。
    • コイン両替問題 II では、「最少硬貨枚数」を求める問題から「硬貨の組合せ数」を求める問題へ変わるため、状態遷移方程式も \\(\\min()\\) から総和演算子へ対応して変わります。

    編集距離問題

    • 編集距離(Levenshtein 距離)は 2 つの文字列間の類似度を測るために用いられ、ある文字列を別の文字列へ変換するための最小編集回数として定義されます。編集操作には追加、削除、置換が含まれます。
    • 編集距離問題の状態は、\\(s\\) の前 \\(i\\) 文字を \\(t\\) の前 \\(j\\) 文字へ変更するのに必要な最小編集回数として定義されます。\\(s[i] \\ne t[j]\\) のときは、追加、削除、置換の 3 つの判断があり、それぞれに対応する残りの部分問題があります。これにより最適部分構造を見いだし、状態遷移方程式を構築できます。一方、\\(s[i] = t[j]\\) のときは現在の文字を編集する必要はありません。
    • 編集距離では、状態は真上、真左、左上の状態に依存します。そのため、空間最適化後は順方向でも逆方向でも正しく状態遷移できません。そこで、変数を 1 つ用いて左上の状態を一時保存し、完全ナップサック問題と等価な形へ変換することで、空間最適化後に順方向走査を行えるようにします。
    ","path":["第 14 章   動的計画法","14.7   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   完全ナップサック問題","text":"

    本節では、まずもう 1 つの代表的なナップサック問題である完全ナップサック問題を解き、その特殊例である硬貨交換問題について見ていきます。

    ","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   完全ナップサック問題のサンプルデータ

    ","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1","level":3,"title":"1.   動的計画法の考え方","text":"

    完全ナップサック問題は 0-1 ナップサック問題と非常によく似ています。違いは、品物の選択回数に制限がない点だけです。

    • 0-1 ナップサック問題では、各品物は 1 つしかないため、品物 \\(i\\) をナップサックに入れた後は先頭 \\(i-1\\) 個の品物からしか選べません。
    • 完全ナップサック問題では、各品物の数は無限であるため、品物 \\(i\\) をナップサックに入れた後も、引き続き先頭 \\(i\\) 個の品物から選べます。

    完全ナップサック問題では、状態 \\([i, c]\\) の変化は 2 つの場合に分けられます。

    • 品物 \\(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":"

    2 つの問題のコードを比較すると、状態遷移の中で 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 の第 1 次元を削除するだけです。

    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":"

    ナップサック問題は動的計画法の代表的な問題群であり、多くの派生問題があります。硬貨交換問題もその 1 つです。

    Question

    \\(n\\) 種類の硬貨が与えられ、\\(i\\) 番目の硬貨の額面は \\(coins[i - 1]\\) 、目標金額は \\(amt\\) です。各硬貨は繰り返し選択できます。目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は \\(-1\\) を返します。例を以下の図に示します。

    図 14-24   硬貨交換問題のサンプルデータ

    ","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_1","level":3,"title":"1.   動的計画法の考え方","text":"

    硬貨交換は完全ナップサック問題の特殊なケースとみなせます。両者には次の対応関係と相違点があります。

    • 2 つの問題は相互に変換でき、「品物」は「硬貨」、「品物の重さ」は「硬貨の額面」、「ナップサック容量」は「目標金額」に対応します。
    • 最適化の目標は逆であり、完全ナップサック問題は品物価値の最大化、硬貨交換問題は硬貨枚数の最小化を目指します。
    • 完全ナップサック問題はナップサック容量を「超えない」解を求めますが、硬貨交換は目標金額に「ちょうど」一致する解を求めます。

    ステップ 1:各ラウンドの選択を考え、状態を定義して、\\(dp\\) テーブルを得る

    状態 \\([i, a]\\) に対応する部分問題は、**先頭 \\(i\\) 種類の硬貨で金額 \\(a\\) を作るための最小硬貨枚数**であり、これを \\(dp[i, a]\\) と表します。

    2 次元 \\(dp\\) テーブルのサイズは \\((n+1) \\times (amt+1)\\) です。

    ステップ 2:最適部分構造を見つけ、状態遷移方程式を導く

    本問の状態遷移方程式は、完全ナップサック問題と比べて次の 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\\) になります。

    硬貨が 1 枚もない場合、任意の \\(> 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
    コードの可視化

    全画面で見る >

    次の図は硬貨交換の動的計画法の過程を示しており、完全ナップサック問題と非常によく似ています。

    <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   硬貨交換問題 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)\\) の 2 次元行列です。

    現在の状態における組合せ数は、現在の硬貨を選ばない場合と選ぶ場合の 2 つの選択肢の組合せ数の和に等しくなります。状態遷移方程式は次のとおりです。

    \\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

    目標金額が \\(0\\) のときは、どの硬貨も選ばなくても目標金額を作れるため、先頭列のすべての \\(dp[i, 0]\\) を \\(1\\) に初期化します。硬貨がないときは、任意の \\(>0\\) の目標金額を作れないため、先頭行のすべての \\(dp[0, a]\\) は \\(0\\) になります。

    ","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 章   グラフ"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"章の内容","text":"
    • 9.1   グラフ
    • 9.2   グラフの基本操作
    • 9.3   グラフの走査
    • 9.4   まとめ
    ","path":["第 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   連結リスト、木、グラフの関係

    ","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#911","level":2,"title":"9.1.1   グラフの一般的な種類と用語","text":"

    辺が方向性を持つかどうかに応じて、無向グラフ(undirected graph)と有向グラフ(directed graph)に分けられます。次の図のとおりです。

    • 無向グラフでは、辺は 2 つの頂点間の「双方向」の接続関係を表します。例えば WeChat や QQ における「友だち関係」です。
    • 有向グラフでは、辺は方向性を持ち、すなわち \\(A \\rightarrow B\\) と \\(A \\leftarrow B\\) の 2 方向の辺は互いに独立です。例えば Weibo や Douyin における「フォロー」と「フォロワー」の関係です。

    図 9-2   有向グラフと無向グラフ

    すべての頂点が連結しているかどうかに応じて、連結グラフ(connected graph)と非連結グラフ(disconnected graph)に分けられます。次の図のとおりです。

    • 連結グラフでは、ある頂点から出発すると、ほかの任意の頂点に到達できます。
    • 非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。

    図 9-3   連結グラフと非連結グラフ

    辺に「重み」の変数を追加すると、次の図に示すような重み付きグラフ(weighted graph)が得られます。例えば『Honor of Kings』のようなモバイルゲームでは、システムが共にプレイした時間に基づいてプレイヤー間の「親密度」を計算します。この親密度ネットワークは重み付きグラフで表せます。

    図 9-4   重み付きグラフと重みなしグラフ

    グラフというデータ構造には、次のような基本用語があります。

    • 隣接(adjacency):2 つの頂点の間に辺が存在するとき、この 2 つの頂点は「隣接している」といいます。上図では、頂点 1 に隣接する頂点は 2、3、5 です。
    • 経路(path):頂点 A から頂点 B までに通過する辺で構成された列を、A から B への「経路」と呼びます。上図では、辺の列 1-5-2-4 は頂点 1 から頂点 4 への 1 本の経路です。
    • 次数(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\\) の行列を用いてグラフを表します。各行(列)は 1 つの頂点を表し、行列要素は辺を表します。\\(1\\) または \\(0\\) を用いて、2 つの頂点の間に辺があるかどうかを示します。

    次の図のように、隣接行列を \\(M\\)、頂点リストを \\(V\\) とすると、行列要素 \\(M[i, j] = 1\\) は頂点 \\(V[i]\\) から頂点 \\(V[j]\\) への辺が存在することを表し、逆に \\(M[i, j] = 0\\) は 2 つの頂点の間に辺がないことを表します。

    図 9-5   グラフの隣接行列による表現

    隣接行列には次の特徴があります。

    • 単純グラフでは、頂点は自分自身とは接続できないため、このとき隣接行列の主対角線上の要素には意味がありません。
    • 無向グラフでは、2 方向の辺は等価であるため、このとき隣接行列は主対角線に関して対称です。
    • 隣接行列の要素を \\(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   グラフの隣接リストによる表現

    隣接リストは実際に存在する辺だけを格納し、辺の総数は通常 \\(n^2\\) よりはるかに小さいため、より省スペースです。しかし、隣接リストでは辺を見つけるために連結リストを走査する必要があるため、時間効率は隣接行列に及びません。

    上図を見ると、隣接リストの構造はハッシュテーブルにおける「連鎖アドレス法」と非常によく似ているため、同様の方法で効率を最適化できます。例えば、連結リストが長い場合は 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   現実世界でよく見られるグラフ

    頂点 辺 グラフ計算問題 ソーシャルネットワーク ユーザー 友だち関係 潜在的な友だちの推薦 地下鉄路線 駅 駅間の接続性 最短経路の推薦 太陽系 天体 天体間の万有引力作用 惑星軌道の計算","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   グラフの基本操作","text":"

    グラフの基本操作は、「辺」に対する操作と「頂点」に対する操作に分けられます。「隣接行列」と「隣接リスト」の 2 つの表現方法では、実装方法が異なります。

    ","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#921","level":2,"title":"9.2.1   隣接行列に基づく実装","text":"

    頂点数が \\(n\\) の無向グラフを与えると、各種操作の実装方法は次図のとおりです。

    • 辺の追加または削除:隣接行列で指定した辺を直接変更すればよく、\\(O(1)\\) 時間です。無向グラフであるため、2 方向の辺を同時に更新する必要があります。
    • 頂点の追加:隣接行列の末尾に 1 行 1 列を追加し、すべてを \\(0\\) で埋めればよく、\\(O(n)\\) 時間です。
    • 頂点の削除:隣接行列から 1 行 1 列を削除します。先頭行と先頭列を削除する場合が最悪で、\\((n-1)^2\\) 個の要素を「左上へ移動」させる必要があるため、\\(O(n^2)\\) 時間です。
    • 初期化:\\(n\\) 個の頂点を受け取り、長さ \\(n\\) の頂点リスト vertices を初期化するのに \\(O(n)\\) 時間、サイズ \\(n \\times n\\) の隣接行列 adjMat を初期化するのに \\(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        # 隣接行列に 1 行追加\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        adjMat.emplace_back(vector<int>(n, 0));\n        // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\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        // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // 隣接行列に 1 列追加\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    // 隣接行列に 1 行追加\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 隣接行列に 1 列追加\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    // 隣接行列に 1 行追加\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        self.adj_mat.push(vec![0; n]);\n        // 隣接行列に 1 列追加\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 列目を 0 にする\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        // 隣接行列に 1 行追加\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // 隣接行列に 1 列追加\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    # 隣接行列に 1 行追加\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # 隣接行列に 1 列追加\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)\\) 時間です。無向グラフなので、2 方向の辺を同時に追加する必要があります。
    • 辺の削除:頂点に対応する連結リストから指定した辺を探して削除するため、\\(O(m)\\) 時間です。無向グラフでは、2 方向の辺を同時に削除する必要があります。
    • 頂点の追加:隣接リストに 1 つの連結リストを追加し、新しい頂点をその連結リストの先頭ノードとするため、\\(O(1)\\) 時間です。
    • 頂点の削除:隣接リスト全体を走査し、指定した頂点を含むすべての辺を削除する必要があるため、\\(O(n + m)\\) 時間です。
    • 初期化:隣接リストに \\(n\\) 個の頂点と \\(2m\\) 本の辺を作成するため、\\(O(n + m)\\) 時間です。
    <1><2><3><4><5>

    図 9-8   隣接リストの初期化、辺の追加と削除、頂点の追加と削除

    以下は隣接リストのコード実装です。上図と比べると、実際のコードには次の違いがあります。

    • 頂点の追加と削除を容易にし、コードを簡潔にするため、連結リストの代わりにリスト(動的配列)を使用しています。
    • ハッシュテーブルを用いて隣接リストを格納しており、key は頂点インスタンス、value はその頂点に隣接する頂点のリスト(連結リスト)です。

    また、隣接リストでは頂点を表すために Vertex クラスを使用しています。その理由は、もし隣接行列と同様にリストのインデックスで異なる頂点を区別すると、インデックス \\(i\\) の頂点を削除する場合、隣接リスト全体を走査して、\\(i\\) より大きいすべてのインデックスを \\(1\\) 減らす必要があり、効率が非常に低いためです。これに対して、各頂点が一意な 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   隣接行列と隣接リストの比較

    隣接行列 隣接リスト(連結リスト) 隣接リスト(ハッシュテーブル) 隣接判定 \\(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)\\)

    上表を見ると、隣接リスト(ハッシュテーブル)の時間効率と空間効率が最も優れているように見えます。しかし実際には、隣接行列のほうが辺の操作効率は高く、必要なのは 1 回の配列アクセスまたは代入だけです。総合的に見ると、隣接行列は「空間を時間と引き換えにする」原則を体現し、隣接リストは「時間を空間と引き換えにする」原則を体現しています。

    ","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   グラフの走査","text":"

    木は「一対多」の関係を表すのに対し、グラフはより高い自由度を持ち、任意の「多対多」の関係を表現できます。したがって、木はグラフの一種の特殊な場合とみなせます。明らかに、木の走査操作もグラフの走査操作の一種の特殊な場合です。

    グラフと木はいずれも、走査操作を実現するために探索アルゴリズムを用いる必要があります。グラフの走査方法も、幅優先走査と深さ優先走査の 2 種類に分けられます。

    ","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931","level":2,"title":"9.3.1   幅優先走査","text":"

    幅優先走査は、近いところから遠いところへ向かう走査方法であり、ある頂点から出発して、常に最も近い頂点を優先して訪問し、層ごとに外側へ広がっていきます。以下の図に示すように、左上の頂点から出発し、まずその頂点のすべての隣接頂点を走査し、次に次の頂点のすべての隣接頂点を走査し、これを繰り返して、すべての頂点を訪問するまで続けます。

    図 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

    ハッシュ集合は、value を持たず key だけを格納するハッシュテーブルとみなせます。これは \\(O(1)\\) の時間計算量で key の追加・削除・検索・更新を行えます。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   グラフの幅優先走査の手順

    幅優先走査の順序列は一意ですか?

    一意ではありません。幅優先走査は「近いところから遠いところへ」の順で走査することだけを要求し、同じ距離にある複数の頂点の走査順は任意に入れ替えて構いません。上図を例にすると、頂点 \\(1\\) と \\(3\\) の訪問順は交換でき、頂点 \\(2\\)、\\(4\\)、\\(6\\) の訪問順も任意に入れ替えられます。

    ","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2","level":3,"title":"2.   計算量の分析","text":"

    時間計算量:すべての頂点は 1 回ずつキューに入り、1 回ずつキューから出るため、\\(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   グラフの深さ優先走査

    ","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 操作は新しい参照を返すため、元の参照を新しい slice の参照で再代入する必要がある\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   グラフの深さ優先走査の手順

    深さ優先走査の順序列は一意ですか?

    幅優先走査と同様に、深さ優先走査の順序列も一意ではありません。ある頂点が与えられたとき、どの方向を先に探索してもよく、つまり隣接頂点の順序は任意に入れ替えられ、それでも深さ優先走査になります。

    木の走査を例にすると、「根 \\(\\rightarrow\\) 左 \\(\\rightarrow\\) 右」「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」「左 \\(\\rightarrow\\) 右 \\(\\rightarrow\\) 根」は、それぞれ先行順、中間順、後行順走査に対応します。これらは 3 種類の走査優先順位を示していますが、いずれも深さ優先走査に属します。

    ","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":"
    • グラフは頂点と辺から構成され、一組の頂点と一組の辺からなる集合として表せます。
    • 線形関係(連結リスト)や分治関係(木)と比べて、ネットワーク関係(グラフ)は自由度が高く、そのぶん複雑です。
    • 有向グラフの辺は方向性を持ち、連結グラフでは任意の頂点に到達でき、重み付きグラフの各辺は重み変数を含みます。
    • 隣接行列は行列を用いてグラフを表し、各行(列)が 1 つの頂点を表し、行列要素が辺を表します。\\(1\\) または \\(0\\) を用いて、2 つの頂点の間に辺があるかないかを示します。隣接行列は追加・削除・検索・更新の操作効率が高い一方で、より多くの空間を消費します。
    • 隣接リストは複数の連結リストを使ってグラフを表し、第 \\(i\\) 個の連結リストが頂点 \\(i\\) に対応し、その頂点に隣接するすべての頂点を格納します。隣接リストは隣接行列よりも省スペースですが、辺を探すために連結リストを走査する必要があるため、時間効率は低くなります。
    • 隣接リスト内の連結リストが長くなりすぎた場合は、赤黒木やハッシュテーブルに変換することで、検索効率を高められます。
    • アルゴリズムの考え方という観点では、隣接行列は「空間を時間と引き換えにする」ことを体現し、隣接リストは「時間を空間と引き換えにする」ことを体現します。
    • グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実のシステムをモデル化するために使えます。
    • 木はグラフの特殊な一例であり、木の走査もグラフ走査の特殊な一例です。
    • グラフの幅優先探索は、近いところから遠いところへ、層ごとに広がっていく探索方法であり、通常はキューを使って実装します。
    • グラフの深さ優先探索は、まず行けるところまで進み、進めなくなったらバックトラックする探索方法であり、通常は再帰に基づいて実装します。
    ","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q:経路の定義は頂点列ですか、それとも辺列ですか?

    Wikipedia では言語版ごとに定義が一致していません。英語版では「経路は辺の列」であり、中国語版では「経路は頂点の列」です。以下は英語版の原文です:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

    本書では、経路を頂点列ではなく辺列とみなします。これは、2 つの頂点の間に複数の辺が存在する可能性があり、その場合は各辺がそれぞれ 1 本の経路に対応するためです。

    Q:非連結グラフには到達できない頂点がありますか?

    非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。非連結グラフ全体を走査するには、グラフ内のすべての連結成分をたどれるように複数の始点を設定する必要があります。

    Q:隣接リストにおいて、「その頂点に接続されたすべての頂点」の順序に決まりはありますか?

    順序は任意でかまいません。ただし実際の応用では、頂点を追加した順序や頂点値の大小順など、特定の規則に従って並べ替える必要がある場合があります。そうすることで、「ある種の極値を持つ」頂点をすばやく見つけやすくなります。

    ","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"第 15 章   貪欲法","text":"

    Abstract

    ヒマワリは太陽に向かって回り、自らが最も大きく成長できる可能性を常に追い求める。

    貪欲戦略は、一回ごとの単純な選択を通じて、徐々に最適な答えへと導く。

    ","path":["第 15 章   貪欲法"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"章の内容","text":"
    • 15.1   貪欲法
    • 15.2   分数ナップサック問題
    • 15.3   最大容量問題
    • 15.4   最大積分割問題
    • 15.5   まとめ
    ","path":["第 15 章   貪欲法"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   分数ナップサック問題","text":"

    Question

    \\(n\\) 個の品物が与えられ、第 \\(i\\) 個の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量が \\(cap\\) のナップサックがある。各品物は 1 回だけ選択できるが、品物の一部を選ぶこともでき、価値は選択した重量の割合に応じて計算される。容量制限の下でナップサック内の品物の最大価値を求めよ。例を以下に示す。

    図 15-3   分数ナップサック問題の例データ

    分数ナップサック問題は 0-1 ナップサック問題と全体として非常によく似ており、状態には現在の品物 \\(i\\) と容量 \\(c\\) が含まれ、目標は容量制限下での最大価値を求めることである。

    異なる点は、本問では品物の一部だけを選べることである。以下に示すように、品物は任意に分割でき、対応する価値は重量の割合に応じて計算される。

    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    # 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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  // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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  # 重さと価値の 2 属性を持つ品物リストを作成する\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(\\log n)\\)、空間計算量は通常 \\(O(\\log n)\\) または \\(O(n)\\) であり、具体的な値はプログラミング言語の実装に依存する。

    ソートを除けば、最悪の場合は品物リスト全体を走査する必要があるため、時間計算量は \\(O(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   分数ナップサック問題の幾何学的表現

    ","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   コイン両替の貪欲戦略

    実装コードは次のとおりです。

    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
    コードの可視化

    全画面で見る >

    思わずこう言いたくなるかもしれません。So clean!貪欲法はわずか十行ほどのコードでコイン両替問題を解いてしまいます。

    ","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)\\) より 1 桁小さいオーダーです。

    しかし、硬貨の額面の組み合わせによっては、貪欲法では最適解を見つけられません。下図に 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   貪欲法では最適解を見つけられない例

    つまり、コイン両替問題に対して、貪欲法は大域最適解を保証できず、非常に悪い解を見つけてしまうこともあります。この問題は動的計画法で解くほうが適しています。

    一般に、貪欲法が適用できる状況は次の 2 つに分けられます。

    1. 最適解を保証できる場合:この場合、貪欲法はしばしば最良の選択です。多くの場合、バックトラッキングや動的計画法より効率的だからです。
    2. 近似最適解を見つけられる場合:この場合も貪欲法は有効です。多くの複雑な問題では、大域最適解を求めること自体が非常に難しく、より高い効率で準最適解を得られるだけでも十分価値があります。
    ","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512","level":2,"title":"15.1.2   貪欲法の特性","text":"

    では、どのような問題が貪欲法に適しているのでしょうか。言い換えると、貪欲法はどのような場合に最適解を保証できるのでしょうか。

    動的計画法と比べると、貪欲法の適用条件はより厳しく、主に次の 2 つの性質に注目します。

    • 貪欲選択性:局所最適な選択が常に大域最適解につながる場合にのみ、貪欲法は最適解を保証できます。
    • 最適部分構造:元の問題の最適解が、部分問題の最適解を含むことです。

    最適部分構造については「動的計画法」の節ですでに紹介したので、ここでは繰り返しません。なお、問題によっては最適部分構造が明確でなくても、貪欲法で解ける場合があります。

    ここでは主に、貪欲選択性をどのように判定するかを考えます。説明だけを見ると単純そうですが、実際には多くの問題で、貪欲選択性を証明するのは容易ではありません。

    たとえばコイン両替問題では、反例を挙げて貪欲選択性が成り立たないことを示すのは簡単ですが、成り立つことを証明するのは難しいです。もし、**どのような条件を満たす硬貨の組み合わせなら貪欲法で解けるのか**と問われると、直感や例示に頼った曖昧な答えしか出せず、厳密な数学的証明を与えるのは困難です。

    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":"

    貪欲法による問題解決の流れは、おおむね次の 3 段階に分けられます。

    1. 問題分析:状態の定義、最適化目標、制約条件などを整理し、問題の性質を理解します。この段階はバックトラッキングや動的計画法でも共通して現れます。
    2. 貪欲戦略の決定:各ステップでどのように貪欲選択を行うかを定めます。この戦略により各ステップで問題規模を縮小し、最終的に問題全体を解決します。
    3. 正しさの証明:通常は、その問題が貪欲選択性と最適部分構造を持つことを示す必要があります。この段階では、帰納法や背理法などの数学的証明が必要になることがあります。

    貪欲戦略を定めることは問題解決の核心ですが、実際には簡単ではないことも多く、主な理由は次のとおりです。

    • 問題ごとに貪欲戦略の差が大きい。多くの問題では貪欲戦略は比較的わかりやすく、おおまかな考察や試行だけで見つけられます。しかし複雑な問題では、貪欲戦略が非常に見えにくいことがあり、その場合は解法経験やアルゴリズム力が大きく問われます。
    • 一見もっともらしい貪欲戦略もある。自信を持って貪欲戦略を設計し、コードを書いて提出しても、一部のテストケースを通過できないことがあります。これは、その貪欲戦略が「部分的にしか正しくない」ためであり、先ほどのコイン両替は典型例です。

    正しさを保証するためには、貪欲戦略に対して厳密な数学的証明を行うべきであり、通常は背理法や数学的帰納法が必要になります。

    しかし、正しさの証明もまた簡単とは限りません。手がかりがない場合には、テストケースを使ってコードをデバッグしながら、貪欲戦略を少しずつ修正して検証していくことがよくあります。

    ","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514","level":2,"title":"15.1.4   貪欲法の典型問題","text":"

    貪欲法は、貪欲選択性と最適部分構造を満たす最適化問題によく用いられます。以下に典型的な貪欲法の問題をいくつか挙げます。

    • 硬貨のお釣り問題:ある種の硬貨の組み合わせでは、貪欲法で常に最適解が得られます。
    • 区間スケジューリング問題:いくつかのタスクがあり、それぞれがある時間区間で実行されるとします。できるだけ多くのタスクを完了することが目標で、毎回終了時刻が最も早いタスクを選ぶなら、貪欲法で最適解を得られます。
    • 分数ナップサック問題:一群の品物と積載容量が与えられたとき、総重量が容量を超えず、かつ総価値が最大になるように品物を選ぶ問題です。毎回、価値対重量比(価値 / 重量)が最も高い品物を選ぶなら、ある条件下で貪欲法は最適解を得られます。
    • 株式売買問題:株価の履歴が与えられ、複数回の売買が可能ですが、すでに株を保有している場合は売却前に再度購入することはできません。目標は最大利益を得ることです。
    • ハフマン符号化:ハフマン符号化は、可逆データ圧縮に用いられる貪欲法です。ハフマン木を構築する際、毎回出現頻度が最も低い 2 つのノードを選んで併合すると、最終的に得られるハフマン木の重み付きパス長(符号長)は最小になります。
    • Dijkstra アルゴリズム:与えられた始点から他の各頂点への最短経路問題を解く貪欲法です。
    ","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   最大容量問題","text":"

    Question

    配列 \\(ht\\) が与えられ、各要素は垂直な仕切り板の高さを表します。配列内の任意の 2 枚の仕切り板と、その間の空間で容器を構成できます。

    容器の容量は高さと幅の積(面積)に等しく、高さは短い方の仕切り板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。

    配列から 2 枚の仕切り板を選び、構成される容器の容量が最大となるようにしてください。最大容量を返します。例を以下の図に示します。

    図 15-7   最大容量問題のサンプルデータ

    容器は任意の 2 枚の仕切り板で囲まれるため、本問の状態は 2 枚の仕切り板のインデックスで表され、\\([i, j]\\) と記します。

    問題の条件より、容量は高さと幅の積に等しく、高さは短い板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。容量を \\(cap[i, j]\\) とすると、計算式は次のようになります。

    \\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

    配列の長さを \\(n\\) とすると、2 枚の仕切り板の組合せ数(状態総数)は \\(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":"

    この問題にはさらに効率的な解法があります。以下の図のように、状態 \\([i, j]\\) を 1 つ選び、インデックスが \\(i < j\\) かつ高さが \\(ht[i] < ht[j]\\) を満たすとします。つまり、\\(i\\) が短い板、\\(j\\) が長い板です。

    図 15-8   初期状態

    以下の図のように、このとき長い板 \\(j\\) を短い板 \\(i\\) に近づけると、容量は必ず小さくなります。

    これは、長い板 \\(j\\) を動かした後は幅 \\(j-i\\) が必ず小さくなるためです。また、高さは短い板で決まるので、高さは変わらない( \\(i\\) が依然として短い板)か、小さくなる(移動後の \\(j\\) が短い板になる)ことしかありません。

    図 15-9   長い板を内側へ動かした後の状態

    逆に考えると、短い板 \\(i\\) を内側へ縮めた場合にのみ、容量が大きくなる可能性があります。幅は必ず小さくなりますが、**高さは大きくなる可能性がある**からです(移動後の短い板 \\(i\\) がより長くなる可能性があります)。たとえば次の図では、短い板を動かした後に面積が大きくなっています。

    図 15-10   短い板を内側へ動かした後の状態

    以上から、本問の貪欲戦略を導けます。2 本のポインタを初期化して容器の両端に置き、各ラウンドで短い板に対応するポインタを内側へ縮め、2 本のポインタが出会うまで続けます。

    以下の図は、貪欲戦略の実行過程を示しています。

    1. 初期状態では、ポインタ \\(i\\) と \\(j\\) は配列の両端にあります。
    2. 現在の状態の容量 \\(cap[i, j]\\) を計算し、最大容量を更新します。
    3. 板 \\(i\\) と板 \\(j\\) の高さを比較し、短い板を内側へ 1 マス移動します。
    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    # 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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  // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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  # 2 枚の板が出会うまで貪欲選択を繰り返す\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\\) を内側へ 1 マス動かすと、次の図に示す状態が「スキップ」されます。これは、その後それらの状態の容量を検証できないことを意味します。

    \\[ 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\\) が与えられたとき、それを少なくとも 2 つの正整数の和に分割し、分割後のすべての整数の積の最大値を求めよ。下図に示す。

    図 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":"

    経験的に、2 つの整数の積はその和より大きくなることが多い。\\(n\\) から因子 \\(2\\) を 1 つ切り出すと、それらの積は \\(2(n-2)\\) となる。この積を \\(n\\) と比較すると、

    \\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

    下図のように、\\(n \\geq 4\\) のとき、\\(2\\) を 1 つ切り出すと積は大きくなる。これは、\\(4\\) 以上の整数はすべて分割すべきことを意味する。

    貪欲戦略一:分割方法に \\(\\geq 4\\) の因子が含まれるなら、それはさらに分割すべきである。最終的な分割方法に現れる因子は \\(1\\)、\\(2\\)、\\(3\\) の 3 種類だけである。

    図 15-14   分割により積が大きくなる

    次に、どの因子が最適かを考える。\\(1\\)、\\(2\\)、\\(3\\) の 3 つの因子のうち、明らかに \\(1\\) が最も悪い。なぜなら \\(1 \\times (n-1) < n\\) は常に成り立ち、\\(1\\) を切り出すとかえって積が小さくなるからである。

    下図のように、\\(n = 6\\) のとき、\\(3 \\times 3 > 2 \\times 2 \\times 2\\) が成り立つ。これは、\\(2\\) を切り出すより \\(3\\) を切り出すほうが有利であることを意味する。

    貪欲戦略二:分割方法の中に存在してよい \\(2\\) は高々 2 つである。なぜなら、3 つの \\(2\\) は常に 2 つの \\(3\\) に置き換えられ、より大きな積を得られるからである。

    図 15-15   最適な分割因子

    以上より、次の貪欲戦略が導かれる。

    1. 整数 \\(n\\) を入力し、余りが \\(0\\)、\\(1\\)、\\(2\\) になるまで、そこから因子 \\(3\\) を繰り返し切り出す。
    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":"

    下図のように、ループで整数を分割する必要はなく、切り捨て除算によって \\(3\\) の個数 \\(a\\) を、剰余演算によって余り \\(b\\) を得られる。このとき、

    \\[ n = 3 a + b \\]

    なお、\\(n \\leq 3\\) の境界ケースでは、必ず \\(1\\) を 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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を例に取ると、よく使われるべき乗計算関数は 3 種類ある。

    • 演算子 ** と関数 pow() の時間計算量はいずれも \\(O(\\log⁡ a)\\) である。
    • 関数 math.pow() は内部で C 言語ライブラリの pow() 関数を呼び出し、浮動小数点のべき乗を実行するため、時間計算量は \\(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\\) :最適な分割方法に \\(\\geq 4\\) の因子 \\(x\\) が存在すると仮定すると、それは必ずさらに \\(2(x-2)\\) に分割でき、より大きい(または等しい)積が得られる。これは仮定に矛盾する。
    2. 分割方法に \\(1\\) は含まれない :最適な分割方法に因子 \\(1\\) が 1 つ存在すると仮定すると、それは必ず別の因子に併合でき、より大きい積を得られる。これは仮定に矛盾する。
    3. 分割方法に含まれる \\(2\\) は高々 2 つ :最適な分割方法に 3 つの \\(2\\) が含まれると仮定すると、それは必ず 2 つの \\(3\\) に置き換えられ、積はより大きくなる。これは仮定に矛盾する。
    ","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":"
    • 貪欲法は通常、最適化問題を解くために用いられ、その原理は各意思決定段階で局所最適な決定を行い、全体最適解を得ることを目指すというものである。
    • 貪欲法は反復的に次々と貪欲な選択を行い、各ラウンドで問題をより小さな部分問題へと変換し、最終的に問題を解決する。
    • 貪欲法は実装が簡単であるだけでなく、問題を解く効率も高い。動的計画法と比べると、貪欲法の時間計算量は通常より低い。
    • 硬貨両替問題では、ある種の硬貨の組み合わせに対しては貪欲法で最適解を保証できるが、別の組み合わせではそうではなく、非常に悪い解を見つけてしまう可能性がある。
    • 貪欲法による解法に適した問題は、貪欲選択性と最適部分構造という 2 つの性質を備えている。貪欲選択性は、貪欲戦略の有効性を表している。
    • 一部の複雑な問題では、貪欲選択性を証明するのは容易ではない。相対的には、反例による否定のほうが簡単であり、硬貨両替問題がその一例である。
    • 貪欲法の問題を解く流れは主に 3 段階に分かれる。すなわち、問題分析、貪欲戦略の決定、正しさの証明である。このうち、貪欲戦略の決定が中核であり、正しさの証明はしばしば難所となる。
    • 分数ナップサック問題は 0-1 ナップサックを基に、品物の一部を選ぶことを許しているため、貪欲法で解くことができる。貪欲戦略の正しさは背理法で証明できる。
    • 最大容量問題は全探索で解くことができ、時間計算量は \\(O(n^2)\\) である。貪欲戦略を設計し、各ラウンドで短い板を内側へ動かすことで、時間計算量を \\(O(n)\\) に最適化できる。
    • 最大分割積問題では、2 つの貪欲戦略を順に導いた。すなわち、\\(\\geq 4\\) の整数はすべてさらに分割すべきであり、最適な分割因子は \\(3\\) である。コードにはべき乗演算が含まれており、時間計算量はその実装方法に依存し、通常は \\(O(1)\\) または \\(O(\\log n)\\) である。
    ","path":["第 15 章   貪欲法","15.5   まとめ"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"第 6 章   ハッシュテーブル","text":"

    Abstract

    コンピュータの世界では、ハッシュテーブルは聡明な図書館員のような存在です。

    彼は請求記号の計算方法を知っており、そのため目的の本を素早く見つけられます。

    ","path":["第 6 章   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"章の内容","text":"
    • 6.1   ハッシュテーブル
    • 6.2   ハッシュ衝突
    • 6.3   ハッシュアルゴリズム
    • 6.4   まとめ
    ","path":["第 6 章   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   ハッシュアルゴリズム","text":"

    前の 2 節では、ハッシュテーブルの動作原理とハッシュ衝突の処理方法を紹介しました。しかし、オープンアドレス法であれ連鎖方式であれ、それらが保証できるのは衝突発生時でもハッシュテーブルが正常に動作することだけであり、ハッシュ衝突そのものを減らすことはできません。

    ハッシュ衝突があまりにも頻繁に発生すると、ハッシュテーブルの性能は急激に劣化します。下図のように、連鎖方式のハッシュテーブルでは、理想的な場合にはキーと値のペアが各バケットに均等に分布し、最良の検索効率を達成します。最悪の場合には、すべてのキーと値のペアが同じバケットに格納され、時間計算量は \\(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":"

    「高速かつ安定した」ハッシュテーブルというデータ構造を実現するために、ハッシュアルゴリズムは次の特徴を備える必要があります。

    • 決定性:同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成しなければなりません。そうして初めて、ハッシュテーブルの信頼性が保たれます。
    • 高効率:ハッシュ値の計算過程は十分に高速であるべきです。計算コストが小さいほど、ハッシュテーブルの実用性は高くなります。
    • 均一分布:ハッシュアルゴリズムは、キーと値のペアがハッシュテーブル内に均等に分布するようにすべきです。分布が均一であるほど、ハッシュ衝突の確率は低くなります。

    実際には、ハッシュアルゴリズムはハッシュテーブルの実装だけでなく、ほかの多くの分野でも広く利用されています。

    • パスワード保存:ユーザーのパスワードを保護するために、システムは通常、平文パスワードを直接保存せず、そのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力内容のハッシュ値を計算し、保存済みのハッシュ値と比較します。一致すれば、そのパスワードは正しいと見なされます。
    • データ完全性検査:送信側はデータのハッシュ値を計算してデータと一緒に送信できます。受信側は受け取ったデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。両者が一致すれば、そのデータは完全だと見なされます。

    暗号分野の応用では、ハッシュ値から元のパスワードを推測するといった逆解析を防ぐために、ハッシュアルゴリズムにはさらに高いレベルの安全性が求められます。

    • 一方向性:ハッシュ値から入力データに関するいかなる情報も逆算できないこと。
    • 耐衝突性:異なる 2 つの入力で同じハッシュ値になるものを見つけることが、極めて困難であること。
    • アバランシェ効果:入力のわずかな変化が、出力の大きく予測不能な変化を引き起こすこと。

    注意してほしいのは、**「均一分布」と「耐衝突性」は独立した 2 つの概念である**という点です。均一分布を満たしていても、耐衝突性を満たすとは限りません。たとえば、入力 key がランダムである場合、ハッシュ関数 key % 100 は均一に分布した出力を生成できます。しかし、このハッシュアルゴリズムはあまりにも単純で、下 2 桁が同じ key はすべて同じ出力になります。そのため、ハッシュ値から利用可能な key を容易に逆算でき、結果としてパスワードが破られてしまいます。

    ","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632","level":2,"title":"6.3.2   ハッシュアルゴリズムの設計","text":"

    ハッシュアルゴリズムの設計は、多くの要素を考慮しなければならない複雑な問題です。しかし、要求の高くない場面であれば、いくつかの単純なハッシュアルゴリズムを設計することもできます。

    • 加算ハッシュ:入力の各文字の ASCII コードを足し合わせ、その合計をハッシュ値とします。
    • 乗算ハッシュ:乗算の非相関性を利用し、各ラウンドで定数を掛けながら、各文字の ASCII コードをハッシュ値に累積します。
    • XOR ハッシュ:入力データの各要素を XOR 演算で 1 つのハッシュ値に累積します。
    • 回転ハッシュ:各文字の ASCII コードを 1 つのハッシュ値に累積し、各回の累積前にハッシュ値を回転させます。
    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\\) で割り切れるため、\\(3\\) で割り切れるすべての key は、\\(0\\)、\\(3\\)、\\(6\\) の 3 つのハッシュ値に写像されます。

    \\[ \\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\\) に置き換えると仮定すると、keymodulus の間に公約数が存在しないため、出力されるハッシュ値の均一性は明らかに向上します。

    \\[ \\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 などの標準的なハッシュアルゴリズムを用いることが一般的です。これらは任意長の入力データを、固定長のハッシュ値へ写像できます。

    ここ 1 世紀近くの間、ハッシュアルゴリズムは継続的に改良と最適化が進められてきました。ある研究者たちは性能向上に取り組み、別の研究者やハッカーたちは安全性の弱点を探し続けてきました。次の表は、実際の応用でよく使われるハッシュアルゴリズムを示したものです。

    • MD5 と SHA-1 は何度も攻撃に成功されているため、各種のセキュリティ用途では廃止されています。
    • SHA-2 系列の SHA-256 は最も安全なハッシュアルゴリズムの 1 つであり、いまだに成功した攻撃例がないため、多くのセキュリティ用途やプロトコルで広く使われています。
    • 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() 関数を呼び出して各種データ型のハッシュ値を計算できます。

    • 整数と真理値のハッシュ値は、その値自身です。
    • 浮動小数点数と文字列のハッシュ値の計算はやや複雑なので、興味がある読者は自分で調べてみてください。
    • タプルのハッシュ値は、各要素のハッシュ値を求めてから、それらを組み合わせて 1 つのハッシュ値にしたものです。
    • オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすれば、内容に基づくハッシュ値を実装できます。

    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 アルゴリズム\"\nhash_str = hash(str)\n# 文字列「Hello アルゴリズム」のハッシュ値は 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 アルゴリズム\";\nsize_t hashStr = hash<string>()(str);\n// 文字列「Hello アルゴリズム」のハッシュ値は 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 アルゴリズム\";\nint hashStr = str.hashCode();\n// 文字列「Hello アルゴリズム」のハッシュ値は -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 アルゴリズム\";\nint hashStr = str.GetHashCode();\n// 文字列「Hello アルゴリズム」のハッシュ値は -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 アルゴリズム\"\nlet hashStr = str.hashValue\n// 文字列「Hello アルゴリズム」のハッシュ値は -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 アルゴリズム\";\nint hashStr = str.hashCode;\n// 文字列「Hello アルゴリズム」のハッシュ値は 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 アルゴリズム\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 文字列「Hello アルゴリズム」のハッシュ値は 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 アルゴリズム\"\nval hashStr = str.hashCode()\n// 文字列「Hello アルゴリズム」のハッシュ値は -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 アルゴリズム\"\nhash_str = str.hash\n# 文字列「Hello アルゴリズム」のハッシュ値は -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%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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":"

    元のハッシュテーブルでは、各バケットには 1 つのキーと値のペアしか格納できません。チェイン法(separate chaining)では、単一要素を連結リストに置き換え、キーと値のペアを連結リストのノードとして扱い、衝突したすべてのキーと値のペアを同じ連結リストに格納します。下図はチェイン法によるハッシュテーブルの例を示しています。

    図 6-5   チェイン法ハッシュテーブル

    チェイン法で実装されたハッシュテーブルでは、操作方法が次のように変わります。

    • 要素の検索:入力 key をハッシュ関数に通してバケットインデックスを得ると、連結リストの先頭ノードにアクセスできます。その後、連結リストを走査して key を比較し、目的のキーと値のペアを探します。
    • 要素の追加:まずハッシュ関数で連結リストの先頭ノードにアクセスし、その後ノード(キーと値のペア)を連結リストに追加します。
    • 要素の削除:ハッシュ関数の結果に基づいて連結リストの先頭にアクセスし、続いて連結リストを走査して対象ノードを探し、削除します。

    チェイン法には次の制約があります。

    • 使用メモリの増加:連結リストにはノードポインタが含まれるため、配列よりも多くのメモリを消費します。
    • 検索効率の低下:対応する要素を見つけるために連結リストを線形走査する必要があるためです。

    以下のコードはチェイン法ハッシュテーブルの簡単な実装を示しています。注意すべき点は 2 つあります。

    • 連結リストの代わりにリスト(動的配列)を使って、コードを簡潔にしています。この設定では、ハッシュテーブル(配列)は複数のバケットを含み、各バケットは 1 つのリストです。
    • 以下の実装にはハッシュテーブルの拡張メソッドが含まれています。負荷率が \\(\\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 を返します。

    下図はオープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布を示しています。このハッシュ関数では、末尾 2 桁が同じ key はすべて同じバケットに写像されます。線形探索によって、それらはそのバケットとその後続のバケットに順に格納されます。

    図 6-6   オープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布

    しかし、**線形探索では「クラスタリング現象」が起こりやすい**です。具体的には、配列内で連続して占有された位置が長いほど、それらの連続位置でハッシュ衝突が発生する可能性が高くなり、さらにその位置の集積成長を促して悪循環を生み、最終的には追加・削除・検索・更新操作の効率低下を招きます。

    注意すべきなのは、**オープンアドレッシングハッシュテーブルでは要素を直接削除できない**ことです。これは、要素を削除すると配列内に空バケット None が生じ、要素を検索するときに線形探索がその空バケットに到達した時点で返ってしまうため、その空バケットより後ろの要素には二度とアクセスできなくなるからです。結果として、プログラムがそれらの要素を存在しないと誤判定する可能性があります。下図のとおりです。

    図 6-7   オープンアドレッシングで要素を削除したことによる検索問題

    この問題を解決するために、遅延削除(lazy deletion)の仕組みを採用できます。これは要素をハッシュテーブルから直接取り除かず、代わりに定数 TOMBSTONE を使ってこのバケットをマークします。この仕組みでは、NoneTOMBSTONE はどちらも空バケットを表し、どちらにもキーと値のペアを配置できます。ただし異なるのは、線形探索が TOMBSTONE に到達した場合は、その先にキーと値のペアが存在する可能性があるため、探索を続けるべきだという点です。

    しかし、遅延削除はハッシュテーブルの性能劣化を加速させる可能性があります。これは、削除操作のたびに削除マークが 1 つ生成され、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 つです。衝突が発生したとき、二次探索では単純に固定歩数を飛ばすのではなく、「探索回数の二乗」に相当する歩数、すなわち \\(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 個のキーと値のペアを格納でき、容量を超えるとオーバーフローバケットを連結します。オーバーフローバケットが多すぎる場合は、性能を確保するために特殊な等量拡張操作を実行します。
    ","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   ハッシュテーブル","text":"

    ハッシュテーブル(hash table)は、散列表とも呼ばれ、キー key と値 value の対応関係を構築することで、高効率な要素検索を実現します。具体的には、ハッシュテーブルにキー key を入力すると、対応する値 value を \\(O(1)\\) 時間で取得できます。

    以下の図に示すように、\\(n\\) 人の学生がいるとし、各学生は「名前」と「学籍番号」の 2 つの情報を持っています。もし「学籍番号を入力すると対応する名前を返す」という検索機能を実現したいなら、下図のようなハッシュテーブルを用いることができます。

    図 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('\\n10583 を削除した後のハッシュテーブル\\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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%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

    ハッシュテーブルには、キーと値のペア、キー、値を走査する 3 つの一般的な方法があります。コード例は以下のとおりです:

    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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%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%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%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":"

    まずは最も単純なケースとして、**1 つの配列だけでハッシュテーブルを実装する**ことを考えます。ハッシュテーブルでは、配列中の各空き位置をバケット(bucket)と呼び、各バケットには 1 つのキーと値のペアを格納できます。したがって、検索操作とは key に対応するバケットを見つけ、そのバケットから value を取得することです。

    では、key に基づいて対応するバケットをどのように特定するのでしょうか。これはハッシュ関数(hash function)によって実現されます。ハッシュ関数の役割は、大きな入力空間をより小さな出力空間に写像することです。ハッシュテーブルでは、入力空間はすべての key 、出力空間はすべてのバケット(配列インデックス)です。言い換えると、key を入力すると、ハッシュ関数によってその key に対応するキーと値のペアの配列内での格納位置を求められます。

    key を入力したとき、ハッシュ関数の計算過程は次の 2 段階に分かれます。

    1. あるハッシュアルゴリズム hash() を用いてハッシュ値を計算します。
    2. ハッシュ値をバケット数(配列長)capacity で剰余し、その key に対応するバケット(配列インデックス)index を求めます。
    index = hash(key) % capacity\n

    その後、index を使ってハッシュテーブル内の対応するバケットにアクセスし、value を取得できます。

    配列長を capacity = 100 、ハッシュアルゴリズムを hash(key) = key とすると、ハッシュ関数は key % 100 となります。次の図では、key を学籍番号、value を名前の例として、ハッシュ関数の動作原理を示します。

    図 6-2   ハッシュ関数の動作原理

    以下のコードは、単純なハッシュテーブルを実装したものです。ここでは、キーと値のペアを表すために keyvalue をクラス 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 の下 2 桁が同じであれば、出力結果も同じになります。たとえば、学籍番号 12836 と 20336 の 2 人の学生を検索すると、次の結果を得ます:

    12836 % 100 = 36\n20336 % 100 = 36\n

    次の図に示すように、2 つの学籍番号が同じ名前を指してしまっており、これは明らかに誤りです。このような、複数の入力が同じ出力に対応する状況をハッシュ衝突(hash collision)と呼びます。

    図 6-3   ハッシュ衝突の例

    容易に分かるように、ハッシュテーブルの容量 \\(n\\) が大きいほど、複数の key が同じバケットに割り当てられる確率は低くなり、衝突も少なくなります。したがって、ハッシュテーブルを拡張することでハッシュ衝突を減らせます。

    次の図に示すように、拡張前はキーと値のペア (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 を入力すると、ハッシュテーブルは \\(O(1)\\) 時間で value を検索でき、非常に高効率である。
    • 一般的なハッシュテーブルの操作には、検索、キーと値のペアの追加、キーと値のペアの削除、ハッシュテーブルの走査などがある。
    • ハッシュ関数は key を配列インデックスに写像し、それによって対応するバケットにアクセスして value を取得する。
    • 異なる 2 つの key が、ハッシュ関数を通した後に同じ配列インデックスになることがあり、検索結果の誤りを引き起こす。この現象をハッシュ衝突と呼ぶ。
    • ハッシュテーブルの容量が大きいほど、ハッシュ衝突の確率は低くなる。そのため、ハッシュテーブルを拡張することでハッシュ衝突を緩和できる。配列の拡張と同様に、ハッシュテーブルの拡張操作のコストは大きい。
    • 負荷率は、ハッシュテーブル内の要素数をバケット数で割ったものと定義され、ハッシュ衝突の深刻さを反映する。ハッシュテーブル拡張を発動する条件としてよく用いられる。
    • 連鎖方式では、単一要素を連結リストに変換し、衝突したすべての要素を同じ連結リストに格納する。しかし、連結リストが長すぎると検索効率が低下するため、さらに連結リストを赤黒木に変換して効率を高めることができる。
    • オープンアドレス法は複数回の探索によってハッシュ衝突を処理する。線形探索は固定のステップ幅を用いるが、要素を削除できず、クラスタリングが発生しやすいという欠点がある。二重ハッシュは複数のハッシュ関数を用いて探索するため、線形探索に比べてクラスタリングが起きにくいが、複数のハッシュ関数によって計算量が増える。
    • プログラミング言語ごとに、異なるハッシュテーブル実装が採用されている。たとえば、Java の HashMap は連鎖方式を使用し、Python の Dict はオープンアドレス法を採用している。
    • ハッシュテーブルでは、ハッシュアルゴリズムに決定性、高効率、均一分布という特徴が求められる。暗号学では、ハッシュアルゴリズムはさらに耐衝突性とアバランシェ効果も備えるべきである。
    • ハッシュアルゴリズムは通常、大きな素数を法として用い、ハッシュ値の均一分布を最大限に保証してハッシュ衝突を減らす。
    • 一般的なハッシュアルゴリズムには 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 章   ヒープ"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"章の内容","text":"
    • 8.1   ヒープ
    • 8.2   ヒープ構築
    • 8.3   Top-k 問題
    • 8.4   まとめ
    ","path":["第 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":"

    まず空のヒープを作成し、次にリストを走査して、各要素に対して順に「ヒープへの挿入操作」を実行します。つまり、要素をヒープの末尾に追加してから、その要素に対して「下から上へ」のヒープ化を行います。

    要素が1つヒープに挿入されるたびに、ヒープの長さは1増加します。ノードは上から下へ順に二分木へ追加されるため、ヒープは「上から下へ」構築されます。

    要素数を \\(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":"

    実際には、より効率的なヒープ構築法を実現でき、全体は2つの手順に分かれます。

    1. リストのすべての要素をそのままヒープに追加します。この時点では、ヒープの性質はまだ満たされていません。
    2. ヒープを逆順で走査し(レベル順走査の逆順)、各非葉ノードに対して順に「上から下へ」のヒープ化を実行します。

    あるノードをヒープ化するたびに、そのノードを根とする部分木は合法な部分ヒープになります。また、逆順で走査するため、ヒープは「下から上へ」構築されます。

    逆順走査を選ぶのは、この方法なら現在のノードの下にある部分木がすでに合法な部分ヒープであることを保証でき、そのうえで現在のノードをヒープ化してはじめて有効になるからです。

    なお、葉ノードには子ノードがないため、それ自体が自然に合法な部分ヒープであり、ヒープ化は不要です。以下のコードが示すように、最後の非葉ノードは最後のノードの親ノードであり、そこから逆順に走査してヒープ化を実行します。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def __init__(self, nums: list[int]):\n    \"\"\"コンストラクタ。入力リストに基づいてヒープを構築する\"\"\"\n    # リスト要素をそのままヒープに追加\n    self.max_heap = nums\n    # 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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        // 葉ノード以外のすべてのノードをヒープ化\n        h.siftDown(i)\n    }\n    return h\n}\n
    my_heap.swift
    /* コンストラクタ。入力リストに基づいてヒープを構築する */\ninit(nums: [Int]) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = nums\n    // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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  // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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        // 葉ノード以外のすべてのノードをヒープ化\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        // 葉ノード以外のすべてのノードをヒープ化\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            // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // 2 つのノードを交換\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            // 2 つのノードを交換\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  # 葉ノード以外のすべてのノードをヒープ化\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":"

    以下では、2つ目のヒープ構築法の時間計算量を求めてみましょう。

    • 完全二分木のノード数を \\(n\\) とすると、葉ノード数は \\((n + 1) / 2\\) です。ここで \\(/\\) は切り捨て除算を表します。したがって、ヒープ化が必要なノード数は \\((n - 1) / 2\\) です。
    • 上から下へのヒープ化の過程では、各ノードは最大で葉ノードまでヒープ化されるため、最大反復回数は二分木の高さ \\(\\log n\\) です。

    上の2つを掛け合わせると、ヒープ構築過程の時間計算量は \\(O(n \\log n)\\) となります。しかし、この見積もりは正確ではありません。二分木では下層のノード数が上層よりはるかに多いという性質を考慮していないためです。

    次に、より正確な計算を行います。計算を簡単にするため、ノード数が \\(n\\) 、高さが \\(h\\) の「満二分木」を仮定します。この仮定は計算結果の正しさに影響しません。

    図 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)は、特定の条件を満たす完全二分木であり、主に次の 2 種類に分けられます。

    • 最小ヒープ(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   ヒープの操作効率

    メソッド名 説明 時間計算量 push() 要素をヒープに追加 \\(O(\\log n)\\) pop() ヒープ頂点の要素を取り出す \\(O(\\log n)\\) peek() ヒープ頂点の要素にアクセス(最大 / 最小ヒープではそれぞれ最大 / 最小値) \\(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# Python の heapq モジュールはデフォルトで最小ヒープを実装している\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// 最大ヒープを初期化(lambda 式で Comparator を変更すればよい)\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// 最大ヒープを初期化(lambda 式で Comparer を変更すればよい)\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// Swift の 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// 最大ヒープを初期化(lambda 式で Comparator を変更すればよい)\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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20min_heap,%20flag%20%3D%20%5B%5D,%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap,%20flag%20%3D%20%5B%5D,%20-1%0A%20%20%20%20%0A%20%20%20%20%23%20Python%20%E7%9A%84%20heapq%20%E6%A8%A1%E5%9D%97%E9%BB%98%E8%AE%A4%E5%AE%9E%E7%8E%B0%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E8%80%83%E8%99%91%E5%B0%86%E2%80%9C%E5%85%83%E7%B4%A0%E5%8F%96%E8%B4%9F%E2%80%9D%E5%90%8E%E5%86%8D%E5%85%A5%E5%A0%86%EF%BC%8C%E8%BF%99%E6%A0%B7%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%B0%86%E5%A4%A7%E5%B0%8F%E5%85%B3%E7%B3%BB%E9%A2%A0%E5%80%92%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%AE%9E%E7%8E%B0%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E5%9C%A8%E6%9C%AC%E7%A4%BA%E4%BE%8B%E4%B8%AD%EF%BC%8Cflag%20%3D%201%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%B0%8F%E9%A1%B6%E5%A0%86%EF%BC%8Cflag%20%3D%20-1%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%201%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%203%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%202%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%205%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%204%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20flag%20*%20max_heap%5B0%5D%20%23%205%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%0A%20%20%20%20%23%20%E5%87%BA%E5%A0%86%E5%85%83%E7%B4%A0%E4%BC%9A%E5%BD%A2%E6%88%90%E4%B8%80%E4%B8%AA%E4%BB%8E%E5%A4%A7%E5%88%B0%E5%B0%8F%E7%9A%84%E5%BA%8F%E5%88%97%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%205%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%204%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%203%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%202%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%201%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%0A%20%20%20%20size%20%3D%20len%28max_heap%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%A0%86%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20not%20max_heap%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%B9%B6%E5%BB%BA%E5%A0%86%0A%20%20%20%20min_heap%20%3D%20%5B1,%203,%202,%205,%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":"

    「二分木」の章で述べたように、完全二分木は配列で表現するのに非常に適しています。ヒープはまさに完全二分木の一種なので、ここでは配列を使ってヒープを格納します。

    配列で二分木を表す場合、要素はノードの値を表し、インデックスは二分木におけるノードの位置を表します。ノード間の参照関係はインデックスの対応式によって実現できます。

    次の図に示すように、インデックス \\(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 がヒープ内のほかの要素より大きい可能性があるため、ヒープ条件が崩れているかもしれません。そのため、挿入ノードから根ノードまでの経路上にある各ノードを修復する必要があります。この操作をヒープ化(heapify)と呼びます。

    ヒープへ追加したノードから始めて、**下から上へヒープ化**を行います。次の図のように、挿入ノードとその親ノードの値を比較し、挿入ノードのほうが大きければそれらを交換します。その後もこの操作を繰り返し、下から上へ各ノードを修復して、根ノードを越えるか交換不要のノードに達した時点で終了します。

    <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        # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // 2 つのノードを交換\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    // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // 2 つのノードを交換\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    # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # 2 つのノードを交換\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. 根ノードから開始し、**上から下へヒープ化**を行う。

    次の図のように、**「上から下へのヒープ化」の方向は「下から上へのヒープ化」と逆**です。根ノードの値を 2 つの子ノードと比較し、最大の子ノードと根ノードを交換します。その後、この操作を繰り返し、葉ノードを越えるか交換不要のノードに達した時点で終了します。

    <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        # 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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    // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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    # 2 つのノードを交換\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

    長さ \\(n\\) の未整列配列 nums が与えられたとき、配列内で最大の \\(k\\) 個の要素を返してください。

    この問題について、まずは発想が比較的直接的な 2 つの解法を紹介し、その後でより効率の高いヒープ解法を紹介します。

    ","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#831","level":2,"title":"8.3.1   方法一:走査による選択","text":"

    以下の図に示すように \\(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","level":2,"title":"8.3.2   方法二:ソート","text":"

    以下の図に示すように、まず配列 nums をソートし、その後で右端の \\(k\\) 個の要素を返すことができます。時間計算量は \\(O(n \\log n)\\) です。

    明らかに、この方法は必要以上の処理を行っています。なぜなら、必要なのは最大の \\(k\\) 個の要素を見つけることだけであり、他の要素をソートする必要はないからです。

    図 8-7   ソートによって最大の k 個の要素を探す

    ","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#833","level":2,"title":"8.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 中国版で「剣指 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. ピンインの先頭文字が \\(r\\) のページを見つけるまで、手順 1. と手順 2. を繰り返します。
    <1><2><3><4><5>

    図 1-1   辞書を引く手順

    辞書を引くという小学生の必須スキルは、実は有名な「二分探索」アルゴリズムそのものです。データ構造の観点では、辞書を整列済みの「配列」とみなせます。アルゴリズムの観点では、上記の一連の辞書引きの操作を「二分探索」とみなせます。

    例2:トランプを整理する。カードゲームをするとき、毎回手札のトランプを小さい順に並べ替える必要があります。その流れは次の図のとおりです。

    1. トランプを「整列済み」と「未整列」の2つの部分に分け、初期状態では一番左の1枚がすでに整列済みだとします。
    2. 未整列部分から1枚のトランプを取り出し、整列済み部分の正しい位置に挿入します。完了すると、左端の2枚は整列済みになります。
    3. 手順 2. を繰り返し、各ラウンドで未整列部分から1枚を整列済み部分へ挿入し、すべてのトランプが整列済みになるまで続けます。

    図 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)とは、データを整理して保存する方式であり、データの内容、データ間の関係、データの操作方法を含み、次のような設計目標があります。

    • 使用する空間をできるだけ少なくし、コンピュータのメモリを節約します。
    • データの操作をできるだけ高速にし、アクセス、追加、削除、更新などを含みます。
    • 簡潔なデータ表現と論理情報を提供し、アルゴリズムが効率よく動作できるようにします。

    データ構造の設計はトレードオフに満ちた過程です。ある面を改善したい場合、別の面で妥協が必要になることがよくあります。以下に 2 つの例を示します。

    • 連結リストは配列に比べてデータの追加や削除がしやすい一方で、データアクセス速度を犠牲にしています。
    • グラフは連結リストに比べてより豊富な論理情報を提供しますが、より大きなメモリ空間を必要とします。
    ","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123","level":2,"title":"1.2.3   データ構造とアルゴリズムの関係","text":"

    以下の図のように、データ構造とアルゴリズムは高度に関連し、密接に結び付いており、具体的には次の 3 つの点に表れます。

    • データ構造はアルゴリズムの土台です。データ構造はアルゴリズムに対して、構造化して格納されたデータと、そのデータを操作する方法を提供します。
    • アルゴリズムはデータ構造に命を吹き込みます。データ構造そのものはデータ情報を保存するだけであり、アルゴリズムと組み合わせて初めて特定の問題を解決できます。
    • アルゴリズムは通常、異なるデータ構造に基づいて実装できますが、実行効率が大きく異なる場合があり、適切なデータ構造を選ぶことが重要です。

    図 1-4   データ構造とアルゴリズムの関係

    データ構造とアルゴリズムは、以下の図に示す組み立てブロックのようなものです。1 セットのブロックには多くの部品が含まれるだけでなく、詳しい組み立て説明書も付いています。説明書に従って一歩ずつ操作すれば、精巧なブロック模型を組み立てられます。

    図 1-5   組み立てブロック

    両者の詳細な対応関係を次の表に示します。

    表 1-1   データ構造とアルゴリズムを組み立てブロックにたとえる

    データ構造とアルゴリズム 組み立てブロック 入力データ まだ組み立てていないブロック データ構造 ブロックの構成形式。形状、大きさ、接続方法などを含む アルゴリズム ブロックを目標の形に組み上げる一連の操作手順 出力データ ブロック模型

    特筆すべき点として、データ構造とアルゴリズムはプログラミング言語から独立しています。だからこそ、本書では複数のプログラミング言語に基づく実装を提供できます。

    慣習的な略称

    実際の議論では、私たちは通常「データ構造とアルゴリズム」を略して「アルゴリズム」と呼びます。たとえば広く知られている LeetCode のアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識を同時に問うています。

    ","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_paperbook/","level":1,"title":"紙の書籍","text":"

    長い時間をかけて磨き上げた『Hello アルゴリズム』の紙の書籍が、ついに発売されました!今の気持ちは、次の一節で表せます:

    風を追い月を追って立ち止まるな、草原の果てには春の山がある。

    以下の動画では紙の書籍を紹介しており、私の考えもいくつか含まれています:

    • データ構造とアルゴリズムを学ぶ重要性。
    • なぜ紙の書籍で Python を選んだのか。
    • 知識共有に対する理解。

    新人 UP 主ですので、ぜひ応援と高評価・チャンネル登録をお願いします~ありがとうございます!

    紙の書籍のスナップショット:

    ","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_2","level":2,"title":"長所と短所","text":"

    紙の書籍ならではの魅力を、簡単にまとめると次のとおりです:

    • フルカラー印刷を採用し、本書の「アニメーション図解」の強みをそのまま活かせます。
    • 紙の素材にもこだわり、色彩を高い精度で再現しつつ、紙の書籍ならではの質感も残しています。
    • 紙の書籍版は Web 版よりも書式が整っており、たとえば図中の数式には斜体を用いています。
    • 価格を上げずに、マインドマップの折り込みページやしおりも付属します。
    • 紙の書籍、Web 版、PDF 版で内容は同期しており、自由に切り替えて読めます。

    Tip

    紙の書籍と Web 版を同期させるのは難しいため、細かな違いが生じる場合があります。ご了承ください!

    もちろん、購入前に検討しておくべき点もいくつかあります:

    • Python 言語を使用しているため、あなたの主言語と合わない可能性があります(Python は疑似コードと捉え、考え方の理解を重視してください)。
    • フルカラー印刷は図解やコードの読みやすさを大きく高める一方で、白黒印刷より価格はやや高くなります。

    Tip

    「印刷品質」と「価格」は、アルゴリズムにおける「時間効率」と「空間効率」のようなもので、両立は容易ではありません。そして私は、「印刷品質」は「時間効率」に当たるため、より重視すべきだと考えています。

    ","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_3","level":2,"title":"購入リンク","text":"

    紙の書籍に興味があれば、ぜひ一冊ご検討ください。新刊の 5 割引を用意していただきましたので、こちらのリンクをご覧いただくか、以下の QR コードをスキャンしてください:

    ","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_4","level":2,"title":"あとがき","text":"

    当初、私は紙の書籍出版に必要な作業量を甘く見ていて、オープンソースプロジェクトをきちんと保守していれば、紙の書籍版も何らかの自動化手段で生成できると思っていました。実際には、紙の書籍の制作フローとオープンソースプロジェクトの更新の仕組みには大きな違いがあり、その間をつなぐには多くの追加作業が必要でした。

    一冊の本の初稿と出版基準を満たす完成稿との間には、なお大きな隔たりがあります。出版社(企画、編集、デザイン、マーケティングなど)と著者が力を合わせ、長い時間をかけて磨き上げていく必要があります。ここで、図霊の企画編集者である王軍花さん、そして人民郵電出版社と図霊コミュニティで本書の出版工程に携わってくださったすべての皆さまに感謝いたします!

    この本があなたの助けになれば幸いです!

    ","path":["紙の書籍"],"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   本書の主な内容

    ","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 によって開発されました。彼の貢献に感謝します。読者により柔軟な読書方法を提供してくれました。

    本書の執筆過程で、私は多くの方々の助けを得ました。

    • 会社での私の指導教員である李汐博士に感謝します。ある対話の中で「すぐに行動しよう」と励ましてくださり、この本を書く決意を固めることができました;
    • 私の恋人であり、本書の最初の読者でもある泡泡に感謝します。アルゴリズム初心者の視点から多くの貴重な提案をしてくれたおかげで、本書はより初心者に適したものになりました;
    • 腾宝、琦宝、飞宝が本書に創造性あふれる名前を付けてくれたことに感謝します。みんなが最初のコード行「Hello World!」を書いた美しい記憶を呼び起こしてくれました;
    • 校铨が知的財産の面で専門的な支援をしてくれたことに感謝します。これは本オープンソース書籍の改善に重要な役割を果たしました;
    • 苏潼が本書の美しい表紙と logo をデザインし、私の完璧主義につき合って何度も辛抱強く修正してくれたことに感謝します;
    • @squidfunk が組版に関する助言を提供してくれたこと、そして彼が開発したオープンソースのドキュメントテーマ Material-for-MkDocs に感謝します。

    執筆の過程で、私はデータ構造とアルゴリズムに関する多くの教材や記事を読みました。これらの作品は本書に優れた手本を与え、本書の内容の正確性と品質を支えてくれました。ここに、すべての先生方と先人たちの卓越した貢献に感謝します!

    本書は手と頭を同時に使う学習方法を提唱しています。この点で私は『手を動かして学ぶ深層学習』から大きな啓発を受けました。ここで読者の皆さんにこの優れた著作を強くお勧めします。

    心から両親に感謝します。いつも支え励ましてくれたからこそ、私はこの興味深いことに取り組む機会を得ることができました。

    ","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 版)または下線付き(Web 版)で示します。たとえば配列(array)のようなものです。文献を読む際に役立つため、覚えておくことをおすすめします。
    • 重要な内容やまとめの文は 太字 で示します。これらの文章には特に注意してください。
    • 特定の意味を持つ語句には“引用符”を付け、曖昧さを避けます。
    • プログラミング言語ごとに用語が一致しない場合、本書では Python を基準とします。たとえば、“空”を表すのに None を使います。
    • 本書では、よりコンパクトなレイアウトのために、言語ごとのコメント規約を一部省略しています。コメントは主に3種類あります。タイトルコメント、内容コメント、複数行コメントです。
    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   アニメーション図解の例

    ","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#023","level":2,"title":"0.2.3   コード実践で理解を深める","text":"

    本書のサンプルコードは GitHub リポジトリ で管理されています。以下の図のように、ソースコードにはテストケースが付いており、ワンクリックで実行できます。

    時間に余裕があれば、コードを見ながら自分で一度書いてみることをおすすめします。学習時間が限られている場合でも、少なくともすべてのコードに目を通し、実行してください。

    コードを読むのに比べて、書く過程のほうが得られるものは多いものです。手を動かしてこそ、本当に学んだことになります。

    図 0-3   コード実行例

    コードを実行する前準備は主に3ステップです。

    第1ステップ:ローカルのプログラミング環境をインストールする。付録のチュートリアルを参照してインストールしてください。すでにインストール済みであれば、この手順は省略できます。

    第2ステップ:コードリポジトリをクローンまたはダウンロードする。 GitHub リポジトリ にアクセスしてください。すでに Git をインストールしている場合は、次のコマンドでこのリポジトリをクローンできます:

    git clone https://github.com/krahets/hello-algo.git\n

    もちろん、以下の図に示す場所で“Download ZIP”ボタンをクリックし、コードの圧縮ファイルを直接ダウンロードしてローカルで展開することもできます。

    図 0-4   リポジトリのクローンとコードのダウンロード

    第3ステップ:ソースコードを実行する。以下の図のように、上部にファイル名が表示されているコードブロックについては、リポジトリの codes フォルダ内に対応するソースコードファイルがあります。ソースコードファイルはワンクリックで実行できるため、不要なデバッグ時間を減らし、学習内容に集中できます。

    図 0-5   コードブロックと対応するソースコードファイル

    ローカルでコードを実行するだけでなく、Web 版では Python コードの可視化実行にも対応しています(pythontutor を利用)。以下の図のように、コードブロックの下にある“可視化実行”をクリックすると表示を展開し、アルゴリズムコードの実行過程を観察できます。また、“全画面表示”をクリックすると、より見やすい閲覧体験が得られます。

    図 0-6   Python コードの可視化実行

    ","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#024","level":2,"title":"0.2.4   質問と議論を通じてともに成長する","text":"

    本書を読んでいて、理解できていない知識点を安易に読み飛ばさないでください。コメント欄で気軽に質問してください。私と仲間たちが誠意をもって回答し、通常は 2 日以内に返信します。

    以下の図のように、Web 版では各章の下部にコメント欄があります。ぜひコメント欄の内容にも目を通してください。一方では、みんなが直面した問題を知ることで知識の抜けを補い、より深い思考を促せます。もう一方では、ほかの仲間の質問にも積極的に答え、見解を共有し、互いの成長を助けてほしいと思います。

    図 0-7   コメント欄の例

    ","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#025","level":2,"title":"0.2.5   アルゴリズム学習ロードマップ","text":"

    全体として見ると、データ構造とアルゴリズムの学習過程は 3 つの段階に分けられます。

    1. 第 1 段階:アルゴリズム入門。さまざまなデータ構造の特徴と使い方に慣れ、異なるアルゴリズムの原理、流れ、用途、効率などを学ぶ必要があります。
    2. 第 2 段階:アルゴリズム問題を解く。まずは人気の高い問題から取り組み、少なくとも 100 問は蓄積して、主流のアルゴリズム問題に慣れることをおすすめします。最初のうちは、“知識の忘却”が課題になるかもしれませんが、心配はいりません。これはごく自然なことです。“エビングハウスの忘却曲線”に沿って問題を復習すれば、通常は 3~5 回繰り返すことでしっかり記憶に定着します。おすすめの問題リストと学習計画は、この GitHub リポジトリ を参照してください。
    3. 第 3 段階:知識体系を構築する。学習面では、アルゴリズムの連載記事、解法フレームワーク、教材などを読むことで、知識体系を継続的に充実させられます。問題演習の面では、トピック別分類、1 問多解、1 解多題といった発展的な戦略も試せます。関連する学習ノウハウは各コミュニティで見つけられます。

    以下の図のように、本書の内容は主に“第 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":"
    • 本書の主な対象読者はアルゴリズム初学者です。すでにある程度の基礎がある場合でも、本書はアルゴリズム知識を体系的に振り返る助けとなり、書中のソースコードは「問題演習用ツール集」としても利用できます。
    • 本書の内容は主に計算量解析、データ構造、アルゴリズムの三部からなり、この分野の大部分のテーマを網羅しています。
    • アルゴリズム初心者にとって、学習初期の段階で入門書を読むことは非常に重要であり、多くの遠回りを避けられます。
    • 本書のアニメーション図解は通常、重要な知識や難しい知識を紹介するために用いられます。本書を読む際は、これらの内容により多く注意を払うべきです。
    • 実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、実際に自分でコードを書くことを強く勧めます。
    • 本書のWeb版の各章にはコメント欄が設けられており、疑問や見解をいつでも共有することを歓迎します。
    ","path":["第 0 章   前書き","0.3   まとめ"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"参考文献","text":"

    [1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).

    [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

    [3] Robert Sedgewick, et al. Algorithms (4th Edition).

    [4] 严蔚敏. データ構造(C 言語版).

    [5] 邓俊辉. データ構造(C++ 言語版、第3版).

    [6] マーク・アレン・ワイス著,陈越訳. データ構造とアルゴリズム解析:Java言語による記述(第3版).

    [7] 程杰. データ構造の話.

    [8] 王争. データ構造とアルゴリズムの美.

    [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

    [10] Aston Zhang, et al. Dive into Deep Learning.

    ","path":["参考文献"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章   探索","text":"

    Abstract

    探索は未知の冒険であり、私たちは神秘的な空間の隅々まで歩き回る必要があるかもしれず、あるいは素早く目標を特定できるかもしれません。

    この探索の旅において、すべての探求が思いもよらなかった答えをもたらすかもしれません。

    ","path":["第 10 章   探索"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"章の内容","text":"
    • 10.1   二分探索
    • 10.2   二分探索の挿入位置
    • 10.3   二分探索の境界
    • 10.4   ハッシュによる最適化戦略
    • 10.5   探索アルゴリズム再考
    • 10.6   まとめ
    ","path":["第 10 章   探索"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   二分探索","text":"

    二分探索(binary search)は分割統治法に基づく効率的な探索アルゴリズムです。データが整列済みである性質を利用し、各ラウンドで探索範囲を半分に縮小し、目標要素を見つけるか探索区間が空になるまで続けます。

    Question

    長さ \\(n\\) の配列 nums が与えられます。要素は小さい順に並んでおり、重複しません。要素 target がこの配列内にある場合はそのインデックスを返し、含まれない場合は \\(-1\\) を返してください。例を次の図に示します。

    図 10-1   二分探索の例

    次の図に示すように、まずポインタ \\(i = 0\\) と \\(j = n - 1\\) を初期化し、それぞれ配列の先頭要素と末尾要素を指すようにして、探索区間 \\([0, n - 1]\\) を表します。角括弧は閉区間を表し、境界値自体を含むことに注意してください。

    次に、以下の 2 つの手順を繰り返します。

    1. 中央のインデックス \\(m = \\lfloor {(i + j) / 2} \\rfloor\\) を計算します。ここで \\(\\lfloor \\: \\rfloor\\) は切り捨てを表します。
    2. nums[m]target の大小関係を判定し、次の 3 つの場合に分かれます。
      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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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
    コードの可視化

    全画面で見る >

    次の図に示すように、2 種類の区間表現では、二分探索アルゴリズムの初期化、ループ条件、区間の縮小操作がそれぞれ異なります。

    「両閉区間」の表現では左右の境界がどちらも閉区間として定義されるため、ポインタ \\(i\\) とポインタ \\(j\\) による区間縮小の操作も対称になります。このほうがミスをしにくいため、一般には「両閉区間」の書き方を推奨します。

    図 10-3   2 種類の区間定義

    ","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 回の除算、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

    長さ \\(n\\) のソート済み配列 nums が与えられ、その中には重複要素が含まれる可能性があります。配列内で最も左にある要素 target のインデックスを返してください。配列にこの要素が含まれない場合は、\\(-1\\) を返します。

    二分探索で挿入位置を求める方法を思い出すと、探索完了後に \\(i\\) は最も左にある target を指します。したがって、挿入位置を探すことの本質は、最も左にある target のインデックスを探すことです。

    挿入位置を探す関数を使って左端境界を求めることを考えます。なお、配列に target が含まれない場合があり、そのときは次の 2 つの結果が起こりえます。

    • 挿入位置のインデックス \\(i\\) が範囲外になる。
    • 要素 nums[i]target と等しくない。

    上の 2 つの状況に当てはまる場合は、直接 \\(-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 の場合のポインタの縮小操作を置き換えることです。ここではコードを省略するので、興味があれば自分で実装してみてください。

    ここでは、より巧妙な 2 つの方法を紹介します。

    ","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1","level":3,"title":"1.   左端境界探索を再利用する","text":"

    実際には、最も左の要素を探す関数を利用して最も右の要素を探せます。具体的には、最も右にある target を探すことを、最も左にある target + 1 を探すことに変換します。

    下図のように、探索完了後、ポインタ \\(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 より小さい最初の要素を指すことになります。

    したがって、下図のように、配列中に存在しない要素を構成して、それを使って左右の境界を探せます。

    • 最も左にある target の探索:target - 0.5 を探すことに変換でき、ポインタ \\(i\\) を返します。
    • 最も右にある target の探索:target + 0.5 を探すことに変換でき、ポインタ \\(j\\) を返します。

    図 10-8   境界の探索を要素の探索に変換する

    ここではコードを省略しますが、次の 2 点に注意が必要です。

    • 与えられた配列には小数が含まれないため、等しい場合をどう処理するかを気にする必要はありません。
    • この方法では小数を導入するため、関数内の変数 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

    長さ \\(n\\) の整列済み配列 nums と要素 target が与えられます。配列には重複要素は存在しません。ここで target を配列 nums に挿入し、その順序を保ちます。配列中にすでに要素 target が存在する場合は、その左側に挿入します。挿入後の配列における target のインデックスを返してください。例を以下の図に示します。

    図 10-4   二分探索の挿入位置の例データ

    前節の二分探索コードを再利用したい場合は、次の二つの問題に答える必要があります。

    問題 1:配列に target が含まれる場合、挿入位置のインデックスはその要素のインデックスですか?

    問題では target を等しい要素の左側に挿入するよう求めているため、新しく挿入された target は元の target の位置に入ります。つまり、配列に target が含まれる場合、挿入位置のインデックスはその target のインデックスです。

    問題 2:配列に target が存在しない場合、挿入位置はどの要素のインデックスですか?

    二分探索の過程をさらに考えると、nums[m] < target のときは \\(i\\) が移動します。これは、ポインタ \\(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 があるかは分かりません。

    問題では目標要素を最も左に挿入する必要があるため、配列中で最も左にある target のインデックスを探す必要があります。まずは以下の図に示す手順で実現することを考えます。

    1. 二分探索を実行し、任意の target のインデックスを得て、これを \\(k\\) とします。
    2. インデックス \\(k\\) から始めて左へ線形探索し、最も左の target を見つけたら返します。

    図 10-5   線形探索による重複要素の挿入位置

    この方法は使用できますが、線形探索を含むため、時間計算量は \\(O(n)\\) です。配列中に重複した target が多い場合、この方法の効率は低くなります。

    次に、二分探索のコードを拡張することを考えます。以下の図に示すように、全体の流れは変えず、各反復でまず中点インデックス \\(m\\) を計算し、その後 targetnums[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] > targetnums[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 となる 2 つの要素を探索し,それらの配列インデックスを返してください。任意の 1 つの解を返せば十分です。

    ","path":["第 10 章   探索","10.4   ハッシュによる最適化戦略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041","level":2,"title":"10.4.1   線形探索:時間と引き換えに空間を節約","text":"

    考えられるすべての組み合わせを直接走査することを考えます。次の図に示すように,2 重ループを開始し,各ラウンドで 2 つの整数の和が target であるかを判定します。そうであれば,それらのインデックスを返します。

    図 10-9   線形探索で 2 数の和を求める

    コードは次のとおりです:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
    def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"方法 1:総当たり列挙\"\"\"\n    # 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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  // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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  # 2重ループのため、時間計算量は 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":"

    ハッシュテーブルを利用し,キーと値をそれぞれ配列要素と要素のインデックスにします。配列をループで走査し,各ラウンドで次の図に示す手順を実行します。

    1. 数値 target - nums[i] がハッシュテーブル内にあるかを判定します。あれば,この 2 つの要素のインデックスを直接返します。
    2. キーと値の組 nums[i] とインデックス i をハッシュテーブルに追加します。
    <1><2><3>

    図 10-10   補助ハッシュテーブルで 2 数の和を求める

    実装コードは次のとおりで,単一ループだけで済みます:

    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)は、データ構造(配列、連結リスト、木、グラフなど)の中から、特定の条件を満たす 1 つまたは複数の要素を探索するために用いられます。

    探索アルゴリズムは、実装の考え方に応じて次の 2 種類に分けられます。

    • データ構造を走査して目標要素を特定する方法。配列、連結リスト、木、グラフの走査などがこれに当たります。
    • データの構成やデータに含まれる事前情報を利用して、要素を効率よく探す方法。二分探索、ハッシュ探索、二分探索木による探索などがこれに当たります。

    これらのトピックはすでに前の章で扱っているため、探索アルゴリズムは私たちにとって見慣れたものです。本節では、より体系的な視点から探索アルゴリズムをあらためて見直します。

    ","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051","level":2,"title":"10.5.1   総当たり探索","text":"

    総当たり探索は、データ構造の各要素を順に調べて目標要素を特定します。

    • “線形探索”は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から始めて、要素を 1 つずつ調べ、目標要素が見つかるか、もう一方の端に達しても見つからないまで続けます。
    • “幅優先探索”と“深さ優先探索”は、グラフと木における 2 つの走査戦略です。幅優先探索は初期ノードから始めて層ごとに探索し、近いところから遠いところへ各ノードを訪れます。深さ優先探索は初期ノードから始めて 1 本の経路を最後までたどり、その後でバックトラックしてほかの経路を試し、データ構造全体を走査し終えるまで続けます。

    総当たり探索の利点は、単純で汎用性が高く、**データの前処理や追加のデータ構造を必要としない**ことです。

    しかし、この種のアルゴリズムの時間計算量は \\(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-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)\\) データの順序性 なし あり あり なし

    探索アルゴリズムの選択は、規模、探索性能の要求、データの問い合わせ頻度や更新頻度などにも左右されます。

    線形探索

    • 汎用性が高く、データの前処理をまったく必要としません。データを 1 回だけ問い合わせればよい場合、ほか 3 つの手法では前処理にかかる時間のほうが、線形探索そのものより長くなることがあります。
    • 規模の小さいデータに適しています。この場合、時間計算量が効率に与える影響は比較的小さいです。
    • データ更新頻度が高い場面に適しています。この手法では、データに対する追加の保守が不要だからです。

    二分探索

    • 大規模データに適しており、効率が安定しています。最悪時間計算量は \\(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 章   ソート"],"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 章   ソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   バブルソート","text":"

    バブルソート(bubble sort)は、隣接する要素を繰り返し比較して交換することで整列を行います。この過程が泡のように下から上へ浮かび上がることから、バブルソートと呼ばれます。

    次の図に示すように、バブル処理は要素の交換操作によってシミュレートできます。配列の最も左の端から右へ走査し、隣接する要素の大小を順に比較して、「左要素 > 右要素」であれば両者を交換します。走査が終わると、最大の要素は配列の最も右端へ移動します。

    <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\\) とすると、バブルソートの手順は次の図のとおりです。

    1. まず、\\(n\\) 個の要素に対して「バブル処理」を行い、配列中の最大要素を正しい位置へ交換します。
    2. 次に、残りの \\(n - 1\\) 個の要素に対して「バブル処理」を行い、2 番目に大きい要素を正しい位置へ交換します。
    3. このようにして、\\(n - 1\\) 回の「バブル処理」を終えると、大きいほうから \\(n - 1\\) 個の要素がすべて正しい位置へ交換されます。
    4. 残った 1 つの要素は必ず最小要素なので、並べ替える必要はなく、これで配列のソートが完了します。

    図 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)\\) の浮動小数点数であるとします。バケットソートの流れを以下の図に示します。

    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":"

    バケットソートは、非常に大規模なデータの処理に適しています。たとえば、入力データに 100 万個の要素が含まれ、空間の制約によりシステムメモリへすべてのデータを一度に読み込めない場合です。このとき、データを 1000 個のバケットに分け、それぞれのバケットを個別にソートしてから、最後に結果を結合できます。

    • 時間計算量は \\(O(n + k)\\) :要素が各バケット内に平均的に分布していると仮定すると、各バケット内の要素数は \\(\\frac{n}{k}\\) です。1 つのバケットをソートするのに \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) の時間がかかるなら、すべてのバケットのソートには \\(O(n \\log\\frac{n}{k})\\) の時間がかかります。バケット数 \\(k\\) が十分大きいとき、時間計算量は \\(O(n)\\) に近づきます。結果を結合する際には、すべてのバケットと要素を走査する必要があり、\\(O(n + k)\\) の時間を要します。最悪の場合、すべてのデータが 1 つのバケットに割り当てられ、そのバケットのソートに \\(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)\\) に達しますが、鍵は要素を各バケットへ均等に分配すること にあります。実際のデータは均一に分布していないことが多いからです。たとえば、Taobao 上のすべての商品を価格帯ごとに 10 個のバケットへ均等に分けたいとしても、商品の価格分布は偏っており、100 元未満は非常に多く、1000 元超は非常に少ないかもしれません。価格区間を単純に 10 等分すると、各バケットの商品数には大きな差が生じます。

    均等な分配を実現するために、まず大まかな境界線を設定し、データをひとまず 3 個のバケットに粗く振り分けます。分配後は、商品数の多いバケットをさらに 3 個のバケットに分割し、すべてのバケット内の要素数がおおむね等しくなるまでこれを続けます。

    以下の図に示すように、この方法の本質は再帰木を構築することにあり、目標は葉ノードの値をできるだけ均等にすることです。もちろん、毎回データを 3 個のバケットに分割する必要はなく、具体的な分け方はデータの特徴に応じて柔軟に選べます。

    図 11-14   再帰的にバケットを分割

    商品価格の確率分布をあらかじめ把握しているなら、データの確率分布に基づいて各バケットの価格境界を設定できます。なお、データ分布は必ずしも特別に統計を取る必要はなく、データの特徴に応じて何らかの確率モデルで近似することもできます。

    以下の図に示すように、商品価格が正規分布に従うと仮定すれば、価格区間を合理的に設定でき、それによって商品を各バケットへ均等に分配できます。

    図 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":"

    まず簡単な例を見てみましょう。長さ \\(n\\) の配列 nums が与えられ、その要素はすべて「非負整数」であるとします。計数ソートの全体的な流れを以下の図に示します。

    1. 配列を走査し、その中の最大値を見つけて \\(m\\) とし、続いて長さ \\(m + 1\\) の補助配列 counter を作成します。
    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 の各インデックスを 1 つのバケットとみなし、個数を数える過程を各要素を対応するバケットへ振り分ける操作とみなせます。本質的には、計数ソートは整数データにおけるバケットソートの特殊な一例です。

    ","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 に対して各反復で次の 2 つの手順を行います。

    1. num を配列 res のインデックス prefix[num] - 1 に格納します。
    2. 累積和 prefix[num] を \\(1\\) 減らし、次に num を配置するインデックスを得ます。

    走査が完了すると、配列 res にソート済みの結果が格納されます。最後に res で元の配列 nums を上書きすれば完了です。以下の図は完全な計数ソートの流れを示しています。

    <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    # 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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 は、res において _num が最後に出現する位置のインデックスである\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  // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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  # 結果配列 res で元の配列 nums を上書きする\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)\\)、非インプレースソート:長さがそれぞれ \\(n\\) と \\(m\\) の配列 rescounter を利用します。
    • 安定ソート: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\\) とすると、ヒープソートの流れは次図のとおりです。

    1. 配列を入力して最大ヒープを構築します。完了後、最大要素はヒープの頂点にあります。
    2. ヒープ頂点の要素(最初の要素)とヒープ末尾の要素(最後の要素)を交換します。交換後、ヒープの長さは \\(1\\) 減少し、整列済み要素数は \\(1\\) 増加します。
    3. ヒープ頂点の要素から始めて、上から下へヒープ化操作(sift down)を実行します。ヒープ化が完了すると、ヒープの性質が回復します。
    4. 2. ステップと第 3. ステップを繰り返し実行します。これを \\(n - 1\\) 回繰り返すと、配列の整列が完了します。

    Tip

    実際には、要素の取り出し操作にも第 2. ステップと第 3. ステップが含まれており、要素を取り出す処理が 1 つ加わるだけです。

    <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        # 2 つのノードを交換\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # ループで上から下へヒープ化\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"ヒープソート\"\"\"\n    # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        swap(nums[i], nums[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid heapSort(vector<int> &nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\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    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid HeapSort(int[] nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfunc heapSort(nums *[]int) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        nums.swapAt(i, ma)\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfunc heapSort(nums: inout [Int]) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfunction heapSort(nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfunction heapSort(nums: number[]): void {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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    // 2 つのノードを交換\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  // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        nums.swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfn heap_sort(nums: &mut [i32]) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\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    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\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    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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    # 2 つのノードを交換\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # ループで上から下へヒープ化\n    i = ma\n  end\nend\n\n### ヒープソート ###\ndef heap_sort(nums)\n  # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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)は単純なソートアルゴリズムであり、その動作原理は手作業でトランプの山を整える過程と非常によく似ています。

    具体的には、未ソート区間から基準要素を 1 つ選び、その要素を左側の整列済み区間の要素と 1 つずつ比較し、正しい位置に挿入します。

    以下の図は、配列に要素を挿入する操作の流れを示しています。基準要素を base とすると、目的のインデックスから base までのすべての要素を 1 つずつ右に移動し、その後 base を目的のインデックスに代入する必要があります。

    図 11-6   1 回の挿入操作

    ","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141","level":2,"title":"11.4.1   アルゴリズムの流れ","text":"

    挿入ソート全体の流れを以下の図に示します。

    1. 初期状態では、配列の 1 番目の要素はすでに整列済みです。
    2. 配列の 2 番目の要素を base として選び、正しい位置に挿入すると、**配列の先頭 2 要素が整列済み**になります。
    3. 3 番目の要素を base として選び、正しい位置に挿入すると、**配列の先頭 3 要素が整列済み**になります。
    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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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)\\) ですが、実際には、挿入ソートはバブルソートや選択ソートよりもはるかに高い頻度で使われます。主な理由は次のとおりです。

    • バブルソートは要素の交換によって実装され、1 つの一時変数を必要とするため、合計で 3 回の基本演算が関わります。これに対して、挿入ソートは要素の代入に基づいており、必要な基本演算は 1 回だけです。したがって、バブルソートの計算コストは通常、挿入ソートより高くなります。
    • 選択ソートの時間計算量はどのような場合でも \\(O(n^2)\\) です。**部分的に整列されたデータが与えられた場合、挿入ソートは通常、選択ソートより効率的**です。
    • 選択ソートは安定ではないため、多段ソートには適用できません。
    ","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   マージソート","text":"

    マージソート(merge sort)は分割統治戦略に基づくソートアルゴリズムであり、以下の図に示す「分割」と「マージ」の段階から構成されます。

    1. 分割段階:再帰によって配列を中点で繰り返し分割し、長い配列のソート問題を短い配列のソート問題へ変換します。
    2. マージ段階:部分配列の長さが 1 になったら分割を終了し、マージを開始して、左右 2 つの短いソート済み配列をより長いソート済み配列へと繰り返しマージしていきます。

    図 11-10   マージソートの分割とマージの段階

    ","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161","level":2,"title":"11.6.1   アルゴリズムの流れ","text":"

    以下の図に示すように、「分割段階」では配列を上から下へ再帰的に中点で 2 つの部分配列へ分割します。

    1. 配列の中点 mid を計算し、左部分配列(区間 [left, mid] )と右部分配列(区間 [mid + 1, right] )を再帰的に分割します。
    2. 手順 1. を再帰的に実行し、部分配列区間の長さが 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)\\) まで最適化できます 。

    • 分割段階:連結リストの分割は「再帰」の代わりに「反復」で実装できるため、再帰で使用するスタックフレーム領域を省けます。
    • マージ段階:連結リストでは、ノードの追加や削除は参照(ポインタ)を変更するだけで実現できるため、マージ段階(2 つの短いソート済み連結リストを 1 つの長いソート済み連結リストにマージすること)では追加の連結リストを作成する必要がありません。

    具体的な実装の詳細は比較的複雑なので、興味のある読者は関連資料を参照して学習してください。

    ","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   クイックソート","text":"

    クイックソート(quick sort)は分割統治戦略に基づくソートアルゴリズムであり、実行効率が高く、広く利用されています。

    クイックソートの中核操作は「パーティション」であり、その目的は、配列内のある要素を「基準数」として選び、基準数より小さいすべての要素を左側へ、大きい要素を右側へ移動することです。具体的には、パーティションの流れを下図に示します。

    1. 配列の最左端の要素を基準数として選び、2 つのポインタ ij を初期化して、それぞれ配列の両端を指すようにします。
    2. ループを設定し、各ラウンドで ij)を使ってそれぞれ基準数より大きい(小さい)最初の要素を探し、その後この 2 つの要素を交換します。
    3. ij が出会うまでステップ 2. を繰り返し、最後に基準数を 2 つの部分配列の境界へ交換します。
    <1><2><3><4><5><6><7><8><9>

    図 11-8   パーティションの手順

    パーティションが完了すると、元の配列は左部分配列、基準数、右部分配列の 3 つに分けられ、「左部分配列の任意の要素 \\(\\leq\\) 基準数 \\(\\leq\\) 右部分配列の任意の要素」を満たします。したがって、次はこの 2 つの部分配列だけをソートすれば済みます。

    クイックソートの分割統治戦略

    パーティションの本質は、長い配列のソート問題を 2 つの短い配列のソート問題へ簡略化することです。

    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    # 基準値を 2 つの部分配列の境界へ交換する\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]); // この 2 つの要素を交換\n    }\n    swap(nums[i], nums[left]);  // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    Swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\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    // 基準値を 2 つの部分配列の境界へ交換する\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) // この 2 つの要素を交換\n    }\n    nums.swapAt(i, left) // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n  }\n  _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する\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        // この 2 つの要素を交換\n        swap(nums, i, j);\n    }\n    // 基準値を 2 つの部分配列の境界へ交換する\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)  // この 2 つの要素を交換\n    }\n    swap(nums, i, left)   // 基準値を 2 つの部分配列の境界へ交換する\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  # 基準値を 2 つの部分配列の境界へ交換する\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":"

    クイックソート全体の流れを下図に示します。

    1. まず、元の配列に対して 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\\) の 2 つの部分配列に分割され、このとき再帰の深さは \\(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)\\) の時間計算量で動作します。
    • キャッシュ利用効率が高い:パーティション操作の実行時には、システムが部分配列全体をキャッシュに読み込めるため、要素アクセスの効率が高くなります。一方、「ヒープソート」のようなアルゴリズムは要素へ飛び飛びにアクセスする必要があり、この性質を持ちません。
    • 計算量の定数係数が小さい:上記 3 つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作総数が最も少なくなります。これは「挿入ソート」が「バブルソート」より速い理由と似ています。
    ","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154","level":2,"title":"11.5.4   基準数の最適化","text":"

    クイックソートは、入力によっては時間効率が低下する可能性があります。極端な例として、入力配列が完全な逆順であるとします。最左端の要素を基準数として選ぶため、パーティション完了後には基準数が配列の最右端へ交換され、左部分配列の長さが \\(n - 1\\)、右部分配列の長さが \\(0\\) になります。この再帰を続けると、各回のパーティション後に必ず一方の部分配列の長さが \\(0\\) となり、分割統治戦略が機能せず、クイックソートは「バブルソート」に近い形へ退化します。

    この状況をできるだけ避けるため、パーティションにおける基準数の選び方を最適化できます。たとえば、ランダムに 1 つの要素を選んで基準数にできます。しかし、運が悪く毎回望ましくない基準数を選んでしまうと、効率は依然として十分ではありません。

    注意すべき点として、プログラミング言語が通常生成するのは「疑似乱数」です。疑似乱数列に合わせて特定のテストケースを構築すると、クイックソートの効率はやはり劣化する可能性があります。

    さらに改善するために、配列から 3 つの候補要素(通常は先頭、末尾、中間の要素)を選び、**その 3 つの候補要素の中央値を基準数とする**ことができます。こうすると、基準数が「小さすぎず大きすぎもしない」確率が大幅に上がります。もちろん、候補要素をさらに増やして、アルゴリズムの頑健性をいっそう高めることも可能です。この方法を採用すると、時間計算量が \\(O(n^2)\\) まで劣化する確率は大きく下がります。

    コード例を以下に示します。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"3つの候補要素の中央値を選ぶ\"\"\"\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    \"\"\"番兵による分割処理(3 点中央値)\"\"\"\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    # 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # 基準値のインデックスを返す\n
    quick_sort.cpp
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint partition(vector<int> &nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\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]); // この 2 つの要素を交換\n    }\n    swap(nums[i], nums[left]);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;                   // 基準値のインデックスを返す\n}\n
    quick_sort.java
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint partition(int[] nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
    quick_sort.cs
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint Partition(int[] nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    Swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
    quick_sort.go
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\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    // 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // 基準値のインデックスを返す\n}\n
    quick_sort.swift
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // 3つの候補要素の中央値を選ぶ\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
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\npartition(nums, left, right) {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
    quick_sort.ts
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\npartition(nums: number[], left: number, right: number): number {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
    quick_sort.dart
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint _partition(List<int> nums, int left, int right) {\n  // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n  }\n  _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n  return i; // 基準値のインデックスを返す\n}\n
    quick_sort.rs
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    i // 基準値のインデックスを返す\n}\n
    quick_sort.c
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint partitionMedian(int nums[], int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i;            // 基準値のインデックスを返す\n}\n
    quick_sort.kt
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // 3つの候補要素の中央値を選ぶ\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)             // この 2 つの要素を交換\n    }\n    swap(nums, i, left)              // 基準値を 2 つの部分配列の境界へ交換する\n    return i                         // 基準値のインデックスを返す\n}\n
    quick_sort.rb
    ### 3 つの候補要素の中央値を選ぶ ###\ndef median_three(nums, left, mid, right)\n  # 3つの候補要素の中央値を選ぶ\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### 3 つの候補要素の中央値を選ぶ ###\ndef median_three(nums, left, mid, right)\n  # 3つの候補要素の中央値を選ぶ\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  # 基準値を 2 つの部分配列の境界へ交換する\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\\) の右部分配列が生成されます。これは、各再帰呼び出しで減る問題サイズが非常に小さいこと(要素が 1 つ減るだけ)を意味し、再帰木の高さは \\(n - 1\\) に達するため、このとき \\(O(n)\\) のスタックフレーム空間を占有します。

    スタックフレーム空間の蓄積を防ぐために、各回のパーティション完了後に 2 つの部分配列の長さを比較し、**短いほうの部分配列に対してのみ再帰**を行えます。短い部分配列の長さは \\(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        # 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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    // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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  # 基準値を 2 つの部分配列の境界へ交換する\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    # 2 つの部分配列のうち短いほうにクイックソートを適用する\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\\) 位としたとき、基数ソートの流れは次図のようになります。

    1. 桁番号 \\(k = 1\\) を初期化します。
    2. 学籍番号の第 \\(k\\) 位に対して「計数ソート」を実行します。完了すると、データは第 \\(k\\) 位に従って昇順に並びます。
    3. \\(k\\) を \\(1\\) 増やし、手順 2. に戻って反復を続けます。すべての桁のソートが完了したら終了します。

    図 11-18   基数ソートのアルゴリズムの流れ

    以下ではコード実装を分解して見ていきます。\\(d\\) 進数の数値 \\(x\\) について、その第 \\(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    \"\"\"要素 num の下から k 桁目を取得(exp = 10^(k-1))\"\"\"\n    # ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"計数ソート(nums の k 桁目でソート)\"\"\"\n    # 10 進数の各桁は 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)  # nums[i] の第 k 位を取得し、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  # d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint Digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunc digit(num, exp int) int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunc countingSortDigit(nums []int, exp int) {\n    // 10 進数の各桁は 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) // nums[i] の第 k 位を取得し、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 // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // 10 進数の各桁は 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) // nums[i] の第 k 位を取得し、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 // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunction digit(num, exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunction countingSortDigit(nums, exp) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunction digit(num: number, exp: number): number {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 `_num` の第 k 桁を取得する。ここで `exp = 10^(k-1)` */\nint digit(int _num, int exp) {\n  // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n  return (_num ~/ exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfn digit(num: i32, exp: i32) -> usize {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return ((num / exp) % 10) as usize;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // 後続のメモリ解放に備えて 0 で初期化する\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < size; i++) {\n        // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfun digit(num: Int, exp: Int): Int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // 10 進数の各桁は 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) // nums[i] の第 k 位を取得し、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 // d の配列内インデックス j を取得する\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
    ### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###\ndef digit(num, exp)\n  # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる\n  (num / exp) % 10\nend\n\n### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###\ndef digit(num, exp)\n  # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる\n  (num / exp) % 10\nend\n\n# ## 計数ソート(nums の k 桁目でソート)###\ndef counting_sort_digit(nums, exp)\n  # 10 進数の各桁は 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) # nums[i] の第 k 位を取得し、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 # d の配列内インデックス j を取得する\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
    コードの可視化

    全画面で見る >

    なぜ最下位桁からソートするのですか?

    連続するソートの各ラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。たとえば、第1ラウンドで \\(a < b\\) となっていても、第2ラウンドで \\(a > b\\) となれば、第2ラウンドの結果が優先されます。数字では高位の優先度が低位より高いため、先に低位をソートし、その後で高位をソートする必要があります。

    ","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\\) とすると、ある1桁に対して計数ソートを実行する時間は \\(O(n + d)\\) であり、全 \\(k\\) 桁をソートする時間は \\(O((n + d)k)\\) です。通常、\\(d\\) と \\(k\\) はどちらも比較的小さいため、時間計算量は \\(O(n)\\) に近づきます。
    • 空間計算量は \\(O(n + d)\\)、非原地ソート:計数ソートと同様に、基数ソートでは長さ \\(n\\) と \\(d\\) の配列 rescounter を補助的に用います。
    • 安定ソート:計数ソートが安定であれば基数ソートも安定です。計数ソートが不安定な場合、基数ソートでは正しいソート結果を保証できません。
    ","path":["第 11 章   ソート","11.10   基数ソート"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   選択ソート","text":"

    選択ソート(selection sort)の仕組みは非常に単純です。ループを開始し、各ラウンドで未ソート区間から最小の要素を選び、整列済み区間の末尾に配置します。

    配列の長さを \\(n\\) とすると、選択ソートの手順は次の図のようになります。

    1. 初期状態では、すべての要素が未ソートであり、未ソートな(インデックス)区間は \\([0, n-1]\\) です。
    2. 区間 \\([0, n-1]\\) 内の最小要素を選び、インデックス \\(0\\) の要素と交換します。これにより、配列の先頭 1 要素が整列済みになります。
    3. 区間 \\([1, n-1]\\) 内の最小要素を選び、インデックス \\(1\\) の要素と交換します。これにより、配列の先頭 2 要素が整列済みになります。
    4. これを繰り返します。\\(n - 1\\) 回の選択と交換を経ると、配列の先頭 \\(n - 1\\) 要素が整列済みになります。
    5. 残った 1 つの要素は必ず最大要素なので、ソートは不要です。これで配列のソートは完了します。
    <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\\) は定数サイズの追加領域しか使用しません。
    • 不安定ソート:次の図のように、要素 nums[i] がそれと等しい要素の右側へ交換され、両者の相対的な順序が変わる可能性があります。

    図 11-3   選択ソートの不安定な例

    ","path":["第 11 章   ソート","11.2   選択ソート"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   ソートアルゴリズム","text":"

    ソートアルゴリズム(sorting algorithm)は、データの集合を特定の順序に従って並べ替えるために用いられます。ソートアルゴリズムは幅広く応用されており、整列済みデータは通常、より効率的に検索、分析、処理できるためです。

    下図に示すように、ソートアルゴリズムにおけるデータ型は整数、浮動小数点数、文字、文字列などです。ソートの判定規則は、数値の大小、文字の ASCII コード順、またはカスタムルールなど、要件に応じて設定できます。

    図 11-1   データ型と判定規則の例

    ","path":["第 11 章   ソート","11.1   ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111","level":2,"title":"11.1.1   評価軸","text":"

    実行効率:ソートアルゴリズムの時間計算量はできるだけ低く、かつ全体の操作回数も少ないこと(時間計算量における定数項が小さいこと)が望まれます。大量データの場合、実行効率はとりわけ重要です。

    インプレース性:その名のとおり、インプレースソートは元の配列を直接操作して並べ替えを行うため、追加の補助配列を必要とせず、メモリを節約できます。通常、インプレースソートはデータの移動操作が少なく、実行速度もより高速です。

    安定性:安定ソートは、並べ替え完了後も、等しい要素の配列内での相対順序が変化しません。

    安定ソートは多段ソートの場面で必要条件となります。学生情報を保存した表があり、第 1 列と第 2 列がそれぞれ氏名と年齢であると仮定します。この場合、不安定ソートによって入力データの順序性が失われる可能性があります。

    # 入力データは氏名順にソートされている\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)\\) に最適化できます。
    • マージソートは分割とマージという 2 つの段階からなり、分割統治戦略を典型的に体現しています。マージソートでは配列を整列する際に補助配列の作成が必要で、空間計算量は \\(O(n)\\) です。一方、連結リストを整列する場合の空間計算量は \\(O(1)\\) まで最適化できます。
    • バケットソートはデータのバケット分配、バケット内ソート、結果の結合という 3 つの手順を含みます。これも分割統治戦略を体現しており、データ量が非常に大きい場合に適しています。バケットソートの鍵は、データを平均的に分配することにあります。
    • カウントソートはバケットソートの特例であり、データの出現回数を数えることで整列を行います。カウントソートはデータ量が大きく、かつデータ範囲が限られている場合に適しており、データを正の整数に変換できることが前提です。
    • 基数ソートは各桁ごとの整列によってデータを整列し、データが固定桁数の数値として表せることを前提とします。
    • 総じて言えば、私たちは高効率で、安定で、インプレースで、さらに適応的であるといった利点を備えたソートアルゴリズムを見つけたいと考えます。しかし、ほかのデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たせるソートアルゴリズムは存在しません。実際の応用では、データの特性に応じて適切なソートアルゴリズムを選ぶ必要があります。
    • 下図では、主流のソートアルゴリズムについて、効率、安定性、インプレース性、適応性などを比較しています。

    図 11-19   ソートアルゴリズムの比較

    ","path":["第 11 章   ソート","11.11   まとめ"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q:ソートアルゴリズムの安定性は、どのような場合に必須ですか?

    現実には、オブジェクトのある属性に基づいて整列することがあります。たとえば、学生には氏名と身長という 2 つの属性があり、多段階のソートを行いたいとします。まず氏名で整列して (A, 180) (B, 185) (C, 170) (D, 170) を得て、その後に身長で整列します。ソートアルゴリズムが不安定である場合、結果は (D, 170) (C, 170) (A, 180) (B, 185) になる可能性があります。

    このように、学生 D と C の位置が入れ替わり、氏名に関する順序性が壊れてしまいます。これは望ましくありません。

    Q:番兵分割において、「右から左へ探索する」順序と「左から右へ探索する」順序は入れ替えられますか?

    できません。最も左端の要素を基準値とする場合は、必ず先に「右から左へ探索する」を行い、その後に「左から右へ探索する」を行う必要があります。この結論はやや直感に反するので、理由を分析してみましょう。

    番兵分割 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] を基準値に選ぶ場合はちょうど逆になり、必ず先に「左から右へ探索する」を行う必要があります。

    Q:クイックソートの再帰深度最適化について、短い配列を選ぶとなぜ再帰深度が \\(\\log n\\) を超えないと保証できるのですか?

    再帰深度とは、現在まだ戻っていない再帰呼び出しの数のことです。各ラウンドの番兵分割では、元の配列を 2 つの部分配列に分けます。再帰深度の最適化後は、下方向に再帰する部分配列の長さは最大でも元の配列長の半分です。最悪の場合でも毎回半分の長さになると仮定すれば、最終的な再帰深度は \\(\\log n\\) になります。

    元のクイックソートを振り返ると、長いほうの配列に対して連続して再帰してしまう可能性があり、最悪の場合は \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) と続き、再帰深度は \\(n\\) になります。再帰深度の最適化により、このような状況を避けられます。

    Q:配列内のすべての要素が等しい場合、クイックソートの時間計算量は \\(O(n^2)\\) になりますか?このような退化はどう処理すべきですか?

    はい。この場合は、番兵分割によって配列を「基準値より小さい」「基準値に等しい」「基準値より大きい」の 3 つの部分に分ける方法を検討できます。下方向に再帰するのは、小さい部分と大きい部分だけです。この方法では、入力要素がすべて等しい配列は、1 回の番兵分割だけで整列を完了できます。

    Q:バケットソートの最悪時間計算量が \\(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 章   スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"章の内容","text":"
    • 5.1   スタック
    • 5.2   キュー
    • 5.3   両端キュー
    • 5.4   まとめ
    ","path":["第 5 章   スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   両端キュー","text":"

    キューでは、先頭要素を削除するか末尾に要素を追加することしかできません。次の図に示すように、両端キュー(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   両端キューの操作効率

    メソッド名 説明 時間計算量 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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20deq.append%282%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E5%B0%BE%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%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E9%A6%96%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%20rear%20%3D%22,%20rear%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_front%20%3D%22,%20pop_front%29%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_rear%20%3D%22,%20pop_rear%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%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":"

    前節を振り返ると、通常の単方向連結リストを使ってキューを実装しました。これは、先頭ノードの削除(デキューに対応)と末尾ノードの後ろへの新規ノード追加(エンキューに対応)を容易に行えるためです。

    両端キューでは、先頭と末尾のどちらでもエンキューとデキューを行えます。言い換えると、両端キューではもう一方の対称方向の操作も実装する必要があります。そのため、両端キューの基盤データ構造として「双方向連結リスト」を採用します。

    次の図に示すように、双方向連結リストの先頭ノードと末尾ノードを両端キューの先頭と末尾と見なし、両端でノードを追加および削除する機能を実現します。

    <1><2><3><4><5>

    図 5-8   連結リストによる両端キューのエンキューとデキュー

    実装コードは次のとおりです:

    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   配列による両端キューのエンキューとデキュー

    キュー実装を土台として、「先頭へのエンキュー」と「末尾からのデキュー」のメソッドを追加するだけで済みます:

    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        # 先頭ポインタを左に 1 つ移動する\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        # 先頭ポインタを 1 つ後ろへ進める\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;        // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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;   // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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   // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを左に 1 つ移動する\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    // 先頭ポインタを 1 つ後ろへ進める\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 // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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; // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを左に 1 つ移動する\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    // 先頭ポインタを右に 1 つ移動する\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,    // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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;       // 先頭ポインタ。先頭要素を指す\n    int queSize;     // 末尾ポインタ。キューの末尾 + 1 を指す\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    // 先頭ポインタを左に 1 つ移動する\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    // 先頭ポインタを 1 つ後ろへ進める\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 // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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    # 先頭ポインタを左に 1 つ移動する\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    # 先頭ポインタを 1 つ後ろへ進める\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":"

    両端キューはスタックとキューの両方の論理を備えているため、これら 2 つのすべての応用場面を実現でき、さらに高い自由度を提供します。

    私たちが知っているように、ソフトウェアの「元に戻す」機能は通常スタックを使って実装されます。システムは変更操作を毎回スタックに push し、その後 pop によって取り消しを実現します。しかし、システム資源の制約を考慮すると、通常ソフトウェアは取り消し可能な手数を制限します(たとえば \\(50\\) 手まで保存可能)。スタックの長さが \\(50\\) を超えると、ソフトウェアはスタックの底部(先頭)で削除操作を行う必要があります。しかしスタックではこの機能を実現できないため、この場合はスタックの代わりに両端キューを使用する必要があります。なお、「元に戻す」の中核ロジック自体は依然としてスタックの後入れ先出し原則に従っており、両端キューは追加のロジックをより柔軟に実装できるだけです。

    ","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   キュー","text":"

    キュー(queue)は、先入れ先出しの規則に従う線形データ構造です。名前のとおり、キューは順番待ちの現象を模したもので、新しく来た人は絶えずキュー末尾に加わり、キュー先頭にいる人から順に離れていきます。

    下図のように、キューの先頭を「キュー先頭」、末尾を「キュー末尾」と呼びます。要素をキュー末尾に加える操作を「エンキュー」、キュー先頭の要素を削除する操作を「デキュー」と呼びます。

    図 5-4   キューの先入れ先出し規則

    ","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521","level":2,"title":"5.2.1   キューの基本操作","text":"

    キューの基本操作を以下の表に示します。なお、メソッド名はプログラミング言語によって異なる場合があります。ここではスタックと同じ命名を採用します。

    表 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 では、キュークラス Qeque は双方向キューであり、キューとしても使用できる\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 と走査メソッドがないため、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%E5%88%9D%E5%A7%8B%E5%8C%96%E9%98%9F%E5%88%97%0A%20%20%20%20%23%20%E5%9C%A8%20Python%20%E4%B8%AD%EF%BC%8C%E6%88%91%E4%BB%AC%E4%B8%80%E8%88%AC%E5%B0%86%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%B1%BB%20deque%20%E7%9C%8B%E4%BD%9C%E9%98%9F%E5%88%97%E4%BD%BF%E7%94%A8%0A%20%20%20%20%23%20%E8%99%BD%E7%84%B6%20queue.Queue%28%29%20%E6%98%AF%E7%BA%AF%E6%AD%A3%E7%9A%84%E9%98%9F%E5%88%97%E7%B1%BB%EF%BC%8C%E4%BD%86%E4%B8%8D%E5%A4%AA%E5%A5%BD%E7%94%A8%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%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%E9%98%9F%E5%88%97%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%90%8E%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%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":"

    下図のように、連結リストの「先頭ノード」と「末尾ノード」をそれぞれ「キュー先頭」と「キュー末尾」とみなし、キュー末尾ではノードの追加のみ、キュー先頭ではノードの削除のみを行うようにします。

    <1><2><3>

    図 5-5   連結リストでキューを実装したエンキューとデキュー操作

    以下は連結リストでキューを実装するコードです:

    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        # キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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    // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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            // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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    // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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    # キューが空なら、先頭ノードと末尾ノードの両方をそのノードに向ける\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)\\) であり、そのままではデキュー操作の効率が低くなります。しかし、次の巧妙な方法によってこの問題を回避できます。

    変数 front を用いてキュー先頭要素のインデックスを指し、さらに変数 size でキューの長さを記録できます。rear = front + size と定義すると、この式で得られる rear はキュー末尾要素の次の位置を指します。

    この設計に基づくと、配列内で要素を含む有効区間は [front, rear - 1] となります。各種操作の実装方法を下図に示します。

    • エンキュー操作:入力要素を rear の位置に代入し、size を 1 増やします。
    • デキュー操作:front を 1 増やし、size を 1 減らすだけです。

    このように、エンキューとデキューはいずれも 1 回の操作だけで済み、時間計算量はともに \\(O(1)\\) です。

    <1><2><3>

    図 5-6   配列でキューを実装したエンキューとデキュー操作

    ここで 1 つ問題があります。エンキューとデキューを繰り返すと、frontrear はどちらも右へ移動し続け、配列の末尾に達するとそれ以上進めなくなります。この問題を解決するために、配列を先頭と末尾がつながった「環状配列」とみなします。

    環状配列では、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  # 先頭ポインタ。先頭要素を指す\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        # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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;       // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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;   // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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   // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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 // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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; // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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,        // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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;       // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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 // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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 # 先頭ポインタ。先頭要素を指す\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    # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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
    コードの可視化

    全画面で見る >

    上記の実装によるキューにも制約があり、長さを可変にできません。しかし、この問題の解決は難しくなく、配列を動的配列に置き換えれば容量拡張の仕組みを導入できます。興味があれば自分で実装してみてください。

    2 つの実装の比較に関する結論はスタックの場合と同じなので、ここでは繰り返しません。

    ","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)は、後入れ先出しの論理に従う線形データ構造です。

    スタックは机の上に積まれた皿の山にたとえられます。1回に1枚の皿しか動かせないとすると、いちばん下の皿を取り出すには、上にある皿を順番にどかす必要があります。この皿をさまざまな型の要素(整数、文字、オブジェクトなど)に置き換えたものが、スタックというデータ構造です。

    下図のように、積み重なった要素の上端を「スタックトップ」、下端を「スタックボトム」と呼びます。要素をスタックトップに追加する操作を「プッシュ」、スタックトップの要素を削除する操作を「ポップ」と呼びます。

    図 5-1   スタックの後入れ先出しの規則

    ","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511","level":2,"title":"5.1.1   スタックの基本操作","text":"

    スタックの基本操作を以下の表に示します。具体的なメソッド名は使用するプログラミング言語によって異なります。ここでは、一般的な 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%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%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%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%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":"

    スタックの動作の仕組みをより深く理解するために、自分でスタッククラスを実装してみましょう。

    スタックは後入れ先出しの原則に従うため、要素の追加や削除はスタックトップでしか行えません。一方、配列や連結リストでは任意の位置で要素を追加・削除できます。つまり、スタックは制限付きの配列または連結リストとみなせます。 言い換えると、配列や連結リストのうち無関係な操作を「隠蔽」することで、外から見た振る舞いをスタックの特性に合わせられます。

    ","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   連結リストによる実装","text":"

    連結リストでスタックを実装する場合、連結リストの先頭ノードをスタックトップ、末尾ノードをスタックボトムとみなせます。

    下図のように、プッシュ操作では要素を連結リストの先頭に挿入するだけでよく、このノード挿入方法は「頭部挿入法」と呼ばれます。ポップ操作では、先頭ノードを連結リストから削除するだけです。

    <1><2><3>

    図 5-2   連結リストによるスタック実装のプッシュ・ポップ操作

    以下は、連結リストによってスタックを実装したコード例です:

    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":"

    配列でスタックを実装する場合、配列の末尾をスタックトップとして扱えます。下図のように、プッシュとポップはそれぞれ配列末尾への要素追加と削除に対応し、どちらの時間計算量も \\(O(1)\\) です。

    <1><2><3>

    図 5-3   配列によるスタック実装のプッシュ・ポップ操作

    プッシュされる要素は際限なく増える可能性があるため、動的配列を使えば、配列の拡張を自前で処理する必要がありません。以下にコード例を示します:

    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-2","level":2,"title":"5.1.3   2つの実装の比較","text":"

    対応する操作

    どちらの実装も、スタックの定義に含まれる各種操作をサポートします。配列ベースの実装はランダムアクセスも可能ですが、これはスタックの定義範囲を超えているため、通常は利用しません。

    時間効率

    配列ベースの実装では、プッシュとポップの両方があらかじめ確保された連続メモリ上で行われるため、キャッシュ局所性が高く、効率に優れます。ただし、プッシュ時に配列容量を超えると拡張処理が発生し、その1回のプッシュの時間計算量は \\(O(n)\\) になります。

    連結リストベースの実装では、サイズ拡張が非常に柔軟であり、前述のような配列拡張による効率低下はありません。ただし、プッシュ時にはノードオブジェクトの初期化とポインタの更新が必要になるため、効率は相対的に低くなります。もっとも、プッシュする要素自体がノードオブジェクトであれば、初期化の手間を省けるため、効率を高められます。

    以上を踏まえると、プッシュおよびポップの対象が intdouble のような基本データ型である場合、次の結論が得られます。

    • 配列ベースのスタックは拡張時に効率が低下しますが、拡張は低頻度の操作であるため、平均効率はより高くなります。
    • 連結リストベースのスタックは、より安定した効率を提供できます。

    空間効率

    リストを初期化するとき、システムは「初期容量」を割り当てますが、この容量は実際の必要量を上回ることがあります。また、拡張は通常、一定の倍率(たとえば2倍)で行われるため、拡張後の容量も実際の必要量を超える可能性があります。したがって、配列ベースのスタックは一定のメモリ浪費を招く可能性があります。

    一方で、連結リストのノードはポインタを追加で保持する必要があるため、連結リストノードは相対的に大きな領域を占有します。

    以上より、どちらの実装がより省メモリかを単純に断定することはできず、具体的な状況に応じて分析する必要があります。

    ","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   スタックの典型的な応用","text":"
    • ブラウザにおける戻ると進む、ソフトウェアにおける取り消しとやり直し。新しいWebページを開くたびに、ブラウザは直前のページをスタックにプッシュするため、戻る操作によって前のページに戻れます。戻る操作は実際にはポップに相当します。戻ると進むを同時にサポートするには、2つのスタックを組み合わせて実現する必要があります。
    • プログラムのメモリ管理。関数を呼び出すたびに、システムはスタックトップにスタックフレームを追加し、関数のコンテキスト情報を記録します。再帰関数では、下向きに再帰していく段階でプッシュが繰り返され、上向きにバックトラックする段階でポップが繰り返されます。
    ","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":"
    • スタックは後入れ先出しの原則に従うデータ構造であり、配列または連結リストで実装できます。
    • 時間効率の面では、スタックの配列実装は平均効率が高い一方、拡張時には 1 回のプッシュ操作の時間計算量が \\(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:ポップした後、そのノードのメモリを解放する必要はありますか?

    後で取り出したノードを引き続き使うのであれば、メモリを解放する必要はありません。以降そのノードを使わない場合でも、JavaPython などの言語には自動ガベージコレクション機構があるため、手動でメモリを解放する必要はありません。一方、CC++ では手動でメモリを解放する必要があります。

    Q:両端キューは 2 つのスタックをつなげたように見えますが、用途は何ですか?

    両端キューは、スタックとキューの組み合わせ、あるいは 2 つのスタックをつなげたもののような構造です。表しているのはスタック + キューのロジックなので、スタックとキューのすべての応用を実現でき、しかもより柔軟です。

    Q:取り消し(undo)とやり直し(redo)は具体的にどのように実装されますか?

    2 つのスタックを使い、スタック A を取り消し用、スタック B をやり直し用に使います。

    1. ユーザーが操作を 1 つ実行するたびに、その操作をスタック A にプッシュし、スタック B を空にします。
    2. ユーザーが「取り消し」を実行したときは、スタック A から直近の操作をポップし、それをスタック B にプッシュします。
    3. ユーザーが「やり直し」を実行したときは、スタック B から直近の操作をポップし、それをスタック A にプッシュします。
    ","path":["第 5 章   スタックとキュー","5.4   まとめ"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章   木","text":"

    Abstract

    大樹は生命力に満ち、根は深く葉は生い茂り、枝は豊かに広がる。

    それはデータ分割統治の生き生きとした姿を私たちに示してくれる。

    ","path":["第 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 章   木"],"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":"

    まずは簡単な例を考えます。与えられた 1 本の充足二分木について、すべてのノードをレベル順走査の順に配列へ格納すると、各ノードは一意な配列インデックスに対応します。

    レベル順走査の性質に基づくと、親ノードのインデックスと子ノードのインデックスの間にある「対応式」を導けます。あるノードのインデックスが \\(i\\) なら、その左子ノードのインデックスは \\(2i + 1\\) 、右子ノードのインデックスは \\(2i + 2\\) です。以下の図は、各ノードインデックス間の対応関係を示しています。

    図 7-12   充足二分木の配列表現

    対応式は、連結リストにおけるノード参照(ポインタ)と同じ役割を果たします。与えられた配列内の任意のノードについて、この対応式を使えばその左(右)子ノードにアクセスできます。

    ","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732","level":2,"title":"7.3.2   任意の二分木を表現する","text":"

    充足二分木は特殊なケースであり、一般の二分木では中間層に多数の None が存在することがよくあります。レベル順走査の列にはこれらの None が含まれないため、その列だけから None の数や分布位置を推定することはできません。つまり、このレベル順走査列に一致する二分木構造は複数存在し得ます。

    次の図のように、非充足二分木が与えられると、上記の配列表現はすでに成り立ちません。

    図 7-13   レベル順走査列に対応する複数の二分木の可能性

    この問題を解決するために、**レベル順走査列にすべての None を明示的に書き込む**ことを考えられます。次の図のように、このように処理すればレベル順走査列で二分木を一意に表現できます。コード例は以下のとおりです:

    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// int のラッパークラス Integer を使えば、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   完全二分木の配列表現

    以下のコードでは、配列ベースで表現した二分木を実装しており、次の操作を含みます。

    • あるノードが与えられたとき、その値、左(右)子ノード、親ノードを取得する。
    • 前順走査、中順走査、後順走査、レベル順走査の列を取得する。
    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)\\) へ劣化します。

    以下の図に示すように、ノード削除を 2 回行うと、この二分探索木は連結リストへ退化します。

    図 7-24   AVL 木がノード削除後に退化する

    別の例として、以下の図に示す完全二分木に 2 つのノードを挿入すると、木は大きく左に傾き、探索操作の時間計算量もそれに伴って劣化します。

    図 7-25   AVL 木がノード挿入後に退化する

    1962 年、G. M. Adelson-Velsky と E. M. 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 木は二分探索木であると同時に平衡二分木でもあり、これら 2 種類の二分木の性質をすべて満たします。したがって、平衡二分探索木(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\\) です。ここでは、ノードの高さを取得・更新するための 2 つの補助関数を用意します:

    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\\) のノードを「不平衡ノード」と呼びます。ノードの不平衡の形に応じて、回転操作は 4 種類に分かれます。右回転、左回転、右回転してから左回転、左回転してから右回転です。以下でこれらを順に説明します。

    ","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   右回転の手順

    以下の図に示すように、ノード child に右の子ノード(grand_child と記す)がある場合、右回転には 1 ステップ追加する必要があります。すなわち、grand_childnode の左の子ノードにします。

    図 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    # child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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  // child を支点として node を右回転させる\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            // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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  # child を支点として node を右回転させる\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_1","level":3,"title":"2.   左回転","text":"

    対応する鏡像として、上記の不平衡二分木を左右反転して考えると、以下の図に示す「左回転」が必要になります。

    図 7-28   左回転

    同様に、以下の図に示すように、ノード child に左の子ノード(grand_child と記す)がある場合、左回転にも 1 ステップ追加する必要があります。すなわち、grand_childnode の右の子ノードにします。

    図 7-29   grand_child を持つ左回転

    分かるように、右回転と左回転は論理的に鏡像対称であり、それぞれが解決する 2 種類の不平衡も対称です。この対称性に基づけば、右回転の実装コードにあるすべての leftright に、すべての rightleft に置き換えるだけで、左回転の実装コードが得られます:

    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    # child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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  // child を支点として node を左回転させる\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            // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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  # child を支点として node を左回転させる\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 では、左回転だけでも右回転だけでも部分木を平衡に戻せません。この場合は、まず child に「左回転」を行い、次に node に「右回転」を行います。

    図 7-30   左回転してから右回転

    ","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4","level":3,"title":"4.   右回転してから左回転","text":"

    以下の図に示すように、上記の不平衡二分木の鏡像のケースでは、まず child に「右回転」を行い、次に node に「左回転」を行います。

    図 7-31   右回転してから左回転

    ","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5","level":3,"title":"5.   回転の選択","text":"

    以下の図に示す 4 種類の不平衡は、上の各ケースにそれぞれ対応しており、必要な操作は順に右回転、左回転してから右回転、右回転してから左回転、左回転です。

    図 7-32   AVL 木の 4 つの回転ケース

    以下の表に示すように、不平衡ノードの平衡係数と、高い側の子ノードの平衡係数の符号を判定することで、その不平衡ノードが上図のどのケースに属するかを判断できます。

    表 7-3   4 種類の回転ケースの選択条件

    不平衡ノードの平衡係数 子ノードの平衡係数 採用すべき回転方法 \\(> 1\\) (左に偏った木) \\(\\geq 0\\) 右回転 \\(> 1\\) (左に偏った木) \\(<0\\) 左回転してから右回転 \\(< -1\\) (右に偏った木) \\(\\leq 0\\) 左回転 \\(< -1\\) (右に偏った木) \\(>0\\) 右回転してから左回転

    使いやすくするために、回転操作を 1 つの関数にカプセル化します。この関数があれば、さまざまな不平衡ケースに対して回転を行い、不平衡ノードを再び平衡に戻せます。コードは次のとおりです:

    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_2","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":"

    以下の図に示すように、二分探索木(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.valnum の大小関係を繰り返し比較します。

    • 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   二分探索木にノードを挿入する

    コード実装では、次の 2 点に注意が必要です。

    • 二分探索木では重複ノードを許可しません。そうでないと定義に反するためです。したがって、挿入対象のノードが木内にすでに存在する場合は、挿入を行わずそのまま返します。
    • ノード挿入を実現するために、ノード 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 の 3 つのケースに分けて対応する削除操作を行います。

    以下の図に示すように、削除対象ノードの次数が \\(0\\) のとき、そのノードは葉ノードであり、直接削除できます。

    図 7-19   二分探索木でノードを削除する(次数 0 )

    以下の図に示すように、削除対象ノードの次数が \\(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 or 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        # tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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    // tmp で cur を上書きする\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 or 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            // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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    # tmp で cur を上書きする\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":"

    以下の図に示すように、二分木の中順走査は「左 \\(\\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\\) 回のループ内で任意のノードを探索できます。

    しかし、二分探索木でノードの挿入と削除を繰り返すと、二分木が以下の図のような連結リストへ退化する可能性があり、このとき各操作の時間計算量も \\(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

    各ノードは 2 つの参照(ポインタ)を持ち、それぞれ左子ノード(left-child node)と右子ノード(right-child node)を指します。このノードはこれら 2 つの子ノードの親ノード(parent node)と呼ばれます。二分木のあるノードが与えられたとき、そのノードの左子ノードとその配下のノードからなる木をそのノードの左部分木(left subtree)と呼び、同様に右部分木(right subtree)が定義されます。

    二分木では、葉ノードを除くすべてのノードが子ノードと空でない部分木を持ちます。以下の図に示すように、「ノード 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":"

    二分木でよく使われる用語を以下の図に示します。

    • 根ノード(root node):二分木の最上位にあるノードで、親ノードを持ちません。
    • 葉ノード(leaf node):子ノードを持たないノードで、2 本のポインタはいずれも None を指します。
    • 辺(edge):2 つのノードを結ぶ線分、すなわちノード参照(ポインタ)です。
    • ノードが属するレベル(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%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%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%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%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%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%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":"

    連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。以下の図に 1 つの例を示します。

    図 7-3   二分木でノードを挿入・削除する

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
    # ノードの挿入と削除\np = TreeNode(0)\n# n1 -> n2 の間にノード P を挿入する\nn1.left = p\np.left = n2\n# ノード P を削除する\nn1.left = n2\n
    binary_tree.cpp
    /* ノードの挿入と削除 */\nTreeNode* P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1->left = P;\nP->left = n2;\n// ノード P を削除する\nn1->left = n2;\n// メモリを解放する\ndelete P;\n
    binary_tree.java
    TreeNode P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.cs
    /* ノードの挿入と削除 */\nTreeNode P = new(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.go
    /* ノードの挿入と削除 */\n// n1 -> n2 の間にノード P を挿入する\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// ノード P を削除する\nn1.Left = n2\n
    binary_tree.swift
    let P = TreeNode(x: 0)\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P\nP.left = n2\n// ノード P を削除する\nn1.left = n2\n
    binary_tree.js
    /* ノードの挿入と削除 */\nlet P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.ts
    /* ノードの挿入と削除 */\nconst P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.dart
    /* ノードの挿入と削除 */\nTreeNode P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.rs
    let p = TreeNode::new(0);\n// n1 -> n2 の間にノード P を挿入する\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// n1 -> n2 の間にノード P を挿入する\nn1->left = P;\nP->left = n2;\n// ノード P を削除する\nn1->left = n2;\n// メモリを解放する\nfree(P);\n
    binary_tree.kt
    val P = TreeNode(0)\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P\nP.left = n2\n// ノード P を削除する\nn1.left = n2\n
    binary_tree.rb
    # ノードの挿入と削除\n_p = TreeNode.new(0)\n# n1 -> n2 の間にノード _p を挿入する\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%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%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%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%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%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%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%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%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%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%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":"

    以下の図に示すように、充足二分木(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":"

    以下の図に示すように、完全二分木(complete binary tree)では最下層のノードだけが完全に埋まっていなくてもよく、しかも最下層のノードは左から右へ連続して詰められていなければなりません。なお、充足二分木も完全二分木の一種です。

    図 7-5   完全二分木

    ","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#3","level":3,"title":"3.   充満二分木","text":"

    以下の図に示すように、充満二分木(full binary tree)では、葉ノードを除くすべてのノードが 2 つの子ノードを持ちます。

    図 7-6   充満二分木

    ","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#4","level":3,"title":"4.   平衡二分木","text":"

    以下の図に示すように、平衡二分木(balanced binary tree)では、任意のノードについて左部分木と右部分木の高さの差の絶対値が 1 を超えません。

    図 7-7   平衡二分木

    ","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#714","level":2,"title":"7.1.4   二分木の退化","text":"

    以下の図は、二分木の理想的な構造と退化した構造を示しています。二分木の各レベルのノードがすべて埋まっていると「充足二分木」となり、すべてのノードが片側に偏ると二分木は「連結リスト」へ退化します。

    • 充足二分木は理想的なケースであり、二分木の「分割統治」の利点を十分に発揮できます。
    • 連結リストはその対極にあり、各種操作はすべて線形操作となり、時間計算量は \\(O(n)\\) まで退化します。

    図 7-8   二分木の最良構造と最悪構造

    以下の表に示すように、最良構造と最悪構造では、二分木の葉ノード数、ノード総数、高さなどが極大または極小になります。

    表 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":"

    次の図に示すように、レベル順走査(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)\\) :すべてのノードを1回ずつ訪問するため、計算量は \\(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)とも呼ばれます。これは「まず行き止まりまで進み、その後で戻って続ける」という走査方法を表しています。

    次の図は、二分木に対して深度優先走査を行う仕組みを示しています。深度優先走査は、二分木全体の外周をぐるりと「一周する」ようなものです。各ノードでは3つの位置に出会い、それぞれが先行順走査・中間順走査・後行順走査に対応します。

    図 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

    深度優先探索は反復によって実装することもできます。興味のある読者は自身で調べてみてください。

    次の図は、二分木の先行順走査における再帰の過程を示しており、「行き」と「帰り」という2つの逆向きの部分に分けられます。

    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)\\) :すべてのノードを1回ずつ訪問するため、計算量は \\(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":"
    • 二分木は非線形データ構造の一種であり、「二分する」分割統治の考え方を体現している。各二分木ノードは 1 つの値と 2 本のポインタを持ち、それぞれ左子ノードと右子ノードを指す。
    • 二分木のあるノードについて、その左(右)子ノードおよびその配下から構成される木を、そのノードの左(右)部分木と呼ぶ。
    • 二分木に関する用語には、根ノード、葉ノード、レベル、次数、辺、高さ、深さなどがある。
    • 二分木の初期化、ノードの挿入、ノードの削除は、連結リストの操作方法と似ている。
    • 一般的な二分木の種類には、perfect 二分木、complete 二分木、full 二分木、平衡二分木がある。perfect 二分木が最も理想的な状態であり、連結リストは退化後の最悪の状態である。
    • 二分木は配列で表現できる。方法としては、ノード値と空き位置をレベル順走査の順に並べ、親ノードと子ノードのインデックス対応関係に基づいてポインタを実現する。
    • 二分木のレベル順走査は幅優先探索の一種であり、「同心円状に外へ広がる」ような逐次的な走査方式を表しており、通常はキューによって実装される。
    • 前順、中順、後順走査はいずれも深さ優先探索に属し、「まず末端まで進み、その後バックトラックして続ける」という走査方式を体現しており、通常は再帰で実装される。
    • 二分探索木は効率的な要素探索データ構造であり、探索、挿入、削除の時間計算量はいずれも \\(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:ノードが 1 つしかない二分木では、木の高さと根ノードの深さはどちらも \\(0\\) ですか?

    はい。高さと深さは通常「通過した辺の本数」として定義されるからです。

    Q:二分木における挿入と削除は通常一連の操作を組み合わせて完了しますが、ここでいう「一連の操作」とは何を指すのでしょうか?リソースの子ノードに対するリソース解放と理解できますか?

    二分探索木を例にすると、ノード削除は 3 つのケースに分けて処理する必要があり、各ケースで複数段階のノード操作が必要になります。

    Q:なぜ DFS による二分木走査には前順・中順・後順の 3 種類があり、それぞれどのような用途があるのですか?

    配列の順方向走査と逆方向走査に似て、前順・中順・後順走査は二分木の 3 つの走査方法であり、特定の順序で走査結果を得るために使えます。たとえば二分探索木では、ノードの大小関係が 左子ノードの値 < 根ノードの値 < 右子ノードの値 を満たすため、「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」の優先順で木を走査すれば、整列済みのノード列を得られます。

    Q:右回転操作は不平衡ノード nodechildgrand_child の関係を処理するものですが、node の親ノードと node の元の接続は維持しなくてよいのですか?右回転後に切れてしまいませんか?

    この問題は再帰の視点から考える必要があります。右回転操作 right_rotate(root) に渡されるのは部分木の根ノードであり、最終的に return child によって回転後の部分木の根ノードを返します。部分木の根ノードとその親ノードの接続は、この関数の返却後に行われるため、右回転操作自身が管理する範囲には含まれません。

    Q:C++ では関数を privatepublic に分けますが、この設計にはどのような考えがありますか?なぜ height() 関数と updateHeight() 関数をそれぞれ publicprivate に置くのですか?

    主に、そのメソッドの利用範囲を見て決めます。メソッドがクラス内部でしか使われないなら、private に設計します。たとえば、利用者が updateHeight() を単独で呼び出しても意味はなく、これは挿入や削除の途中の 1 ステップにすぎません。一方で height() はノードの高さにアクセスするためのもので、vector.size() に似た役割を持つため、使いやすいように public に設定します。

    Q:入力データの集合から二分探索木をどのように構築しますか?根ノードの選び方は重要ですか?

    はい。木の構築方法は、二分探索木のコード中の build_tree() メソッドですでに示されています。根ノードの選択については、通常は入力データをソートし、その中央の要素を根ノードにしてから、左右の部分木を再帰的に構築します。こうすることで、木の平衡性を最大限に保てます。

    Q:Java では、文字列比較には必ず equals() メソッドを使うべきですか?

    Java では、基本データ型については == を使って 2 つの変数の値が等しいかどうかを比較します。参照型については、この 2 つの記法の働き方は異なります。

    • == :2 つの変数が同じオブジェクトを指しているか、つまりメモリ上の位置が同じかどうかを比較するために使います。
    • equals():2 つのオブジェクトの値が等しいかどうかを比較するために使います。

    したがって、値を比較したい場合は equals() を使うべきです。ただし、String a = \"hi\"; String b = \"hi\"; によって初期化された文字列は文字列定数プールに格納され、同じオブジェクトを指すため、a == b でも 2 つの文字列の内容を比較できます。

    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 章   付録"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"章の内容","text":"
    • 16.1   プログラミング環境のインストール
    • 16.2   一緒に制作に参加しましょう
    • 16.3   用語集
    ","path":["第 16 章   付録"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   一緒に制作に参加しましょう","text":"

    著者の力には限りがあるため、本書にはどうしても一部の漏れや誤りが含まれる可能性があります。ご了承ください。誤字、リンク切れ、内容の欠落、表現の曖昧さ、説明の不明瞭さ、文章構成の不適切さなどの問題を見つけた場合は、ぜひ修正にご協力ください。読者により良い学習リソースを提供できます。

    すべての寄稿者の GitHub ID は、本書のリポジトリ、Web 版、PDF 版のホームページに掲載され、オープンソースコミュニティへの惜しみない貢献に感謝を表します。

    オープンソースの魅力

    紙の書籍では、2 回の増刷の間隔が長くなりがちで、内容更新は非常に不便です。

    一方、このオープンソース書籍では、内容更新のサイクルは数日、場合によっては数時間にまで短縮されています。

    ","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/contribution/#1","level":3,"title":"1.   内容の微調整","text":"

    以下の図のように、各ページの右上には「編集アイコン」があります。次の手順で本文やコードを修正できます。

    1. 「編集アイコン」をクリックし、「このリポジトリを Fork する必要があります」と表示された場合は、その操作を承認してください。
    2. Markdown のソースファイルを修正し、内容が正しいことを確認したうえで、できるだけ書式の統一を保ってください。
    3. ページ下部に修正内容の説明を入力し、その後「Propose file change」ボタンをクリックします。ページ遷移後、「Create 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. ローカルで行った変更を Commit し、その後リモートリポジトリへ Push します。
    5. リポジトリのページを更新し、「Create 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":"

    オープンソースで軽量な VS Code をローカルの統合開発環境(IDE)として使用することを推奨します。VS Code 公式サイト にアクセスし、使用している OS に応じたバージョンの VS Code をダウンロードしてインストールしてください。

    図 16-1   公式サイトから VS Code をダウンロード

    VS Code には強力な拡張機能のエコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。Python を例にすると、「Python Extension Pack」拡張機能をインストールした後、Python コードをデバッグできるようになります。インストール手順を以下に示します。

    図 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   データ構造とアルゴリズムの重要用語

    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 ビッグオー記法 asymptotic upper bound 漸近上界 sign-magnitude 符号絶対値表現 1’s complement 1の補数 2’s complement 2の補数 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 AVL 木 red-black tree 赤黒木 level-order traversal レベル順走査 breadth-first traversal 幅優先走査 depth-first traversal 深さ優先走査 binary search tree 二分探索木 balanced binary search tree 平衡二分探索木 balance factor 平衡係数 heap ヒープ max heap 最大ヒープ min heap 最小ヒープ priority queue 優先度付きキュー heapify ヒープ化 top-\\(k\\) problem Top-\\(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 章   配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"章の内容","text":"
    • 4.1   配列
    • 4.2   連結リスト
    • 4.3   リスト
    • 4.4   メモリとキャッシュ *
    • 4.5   まとめ
    ","path":["第 4 章   配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   配列","text":"

    配列(array)は線形データ構造の一種であり、同じ型の要素を連続したメモリ領域に格納します。要素が配列内にある位置を、その要素のインデックス(index)と呼びます。下図は、配列の主要な概念と格納方式を示しています。

    図 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":"

    必要に応じて、配列の初期化方法として初期値なしと初期値ありの 2 種類を使い分けられます。初期値を指定しない場合、多くのプログラミング言語では配列要素は \\(0\\) に初期化されます。

    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%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%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   配列要素のメモリアドレスの計算

    上図を見ると、配列の最初の要素のインデックスは \\(0\\) であり、これは少し直感に反するように思えます。というのも、\\(1\\) から数え始めるほうが自然だからです。しかし、アドレス計算式の観点では、**インデックスの本質はメモリアドレスのオフセット**です。先頭要素のアドレスのオフセットは \\(0\\) であるため、そのインデックスが \\(0\\) なのは妥当です。

    配列では要素へのアクセスは非常に効率的であり、\\(O(1)\\) 時間で任意の要素にランダムアクセスできます。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def random_access(nums: list[int]) -> int:\n    \"\"\"要素へランダムアクセス\"\"\"\n    # 区間 [0, len(nums)-1] からランダムに数字を 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) からランダムに 1 つの数を選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに数字を 1 つ選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに数字を 1 つ選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに 1 つの数を選ぶ\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()) からランダムに数字を 1 つ選ぶ\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) からランダムに 1 つの数を選ぶ\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) からランダムに数字を 1 つ選ぶ\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) からランダムに 1 つの数を選ぶ\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":"

    配列要素はメモリ内で「ぴったり隣接して」おり、その間にほかのデータを格納する余地はありません。下図のように、配列の途中に要素を挿入したい場合は、その要素より後ろにあるすべての要素を 1 つずつ後ろへずらし、その後でそのインデックスに要素を代入する必要があります。

    図 4-3   配列への要素挿入の例

    注意すべき点として、配列の長さは固定であるため、要素を 1 つ挿入すると配列末尾の要素が必ず「失われ」ます。この問題の解決策は「リスト」の章で扱います。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def insert(nums: list[int], num: int, index: int):\n    \"\"\"配列の index 番目に要素 num を挿入\"\"\"\n    # インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # index の要素に num を代入する\n    nums[index] = num\n
    array.cpp
    /* 配列の index 番目に要素 num を挿入 */\nvoid insert(int *nums, int size, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.java
    /* 配列の index 番目に要素 num を挿入 */\nvoid insert(int[] nums, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.cs
    /* 配列の index 番目に要素 num を挿入 */\nvoid Insert(int[] nums, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.go
    /* 配列の index 番目に要素 num を挿入 */\nfunc insert(nums []int, num int, index int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
    array.swift
    /* 配列の index 番目に要素 num を挿入 */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
    array.js
    /* 配列の index 番目に要素 num を挿入 */\nfunction insert(nums, num, index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.ts
    /* 配列の index 番目に要素 num を挿入 */\nfunction insert(nums: number[], num: number, index: number): void {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.dart
    /* 配列の添字 index に要素 _num を挿入 */\nvoid insert(List<int> nums, int _num, int index) {\n  // インデックス index 以降の全要素を 1 つ後ろへ移動する\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
    /* 配列の index 番目に要素 num を挿入 */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.c
    /* 配列の index 番目に要素 num を挿入 */\nvoid insert(int *nums, int size, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
    array.kt
    /* 配列の index 番目に要素 num を挿入 */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
    array.rb
    ### 配列のインデックス index に要素 num を挿入 ###\ndef insert(nums, num, index)\n  # インデックス index 以降の全要素を 1 つ後ろへ移動する\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # index の要素に num を代入する\n  nums[index] = num\nend\n
    コードの可視化

    全画面で見る >

    ","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4","level":3,"title":"4.   要素の削除","text":"

    同様に、下図のように、インデックス \\(i\\) の要素を削除したい場合は、インデックス \\(i\\) より後ろの要素をすべて 1 つずつ前へずらす必要があります。

    図 4-4   配列からの要素削除の例

    注意してください。要素の削除が完了すると、もともとの末尾要素は「意味を持たない」状態になるため、わざわざ変更する必要はありません。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
    def remove(nums: list[int], index: int):\n    \"\"\"index の要素を削除する\"\"\"\n    # インデックス index より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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 より後ろの全要素を 1 つ前へ移動する\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// JavaScript の Array は動的配列であり、直接拡張できます\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// TypeScript の Array は動的配列であり、直接拡張できます\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# Ruby の Array は動的配列であり、直接拡張できます\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 コード値をインデックスとし、対応する要素を配列の対応位置に格納できます。
    • 機械学習:ニューラルネットワークでは、ベクトル、行列、テンソル間の線形代数演算が大量に使われ、これらのデータはいずれも配列の形で構築されます。配列はニューラルネットワークプログラミングで最もよく使われるデータ構造です。
    • データ構造の実装:配列はスタック、キュー、ハッシュテーブル、ヒープ、グラフなどのデータ構造の実装に利用できます。たとえば、グラフの隣接行列表現は実際には 2 次元配列です。
    ","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   連結リスト","text":"

    メモリ空間はすべてのプログラムに共通の資源であり、複雑なシステム実行環境では、空きメモリがメモリの各所に散在している可能性があります。配列を格納するメモリ空間は連続していなければなりませんが、配列が非常に大きい場合、メモリはそのような大きな連続領域を提供できないことがあります。このとき、連結リストの柔軟性という利点が現れます。

    連結リスト(linked list)は線形データ構造の一種であり、各要素はノードオブジェクトです。各ノードは「参照」によって接続されます。参照には次のノードのメモリアドレスが記録されており、これによって現在のノードから次のノードへアクセスできます。

    連結リストの設計では、各ノードをメモリの各所に分散して格納でき、それらのメモリアドレスは連続している必要がありません。

    図 4-5   連結リストの定義と格納方式

    上図を見ると、連結リストの構成単位はノード(node)オブジェクトです。各ノードは 2 つのデータ、すなわちノードの「値」と次のノードを指す「参照」を含みます。

    • 連結リストの最初のノードを「先頭ノード」、最後のノードを「末尾ノード」と呼びます。
    • 末尾ノードが指す先は「空」であり、Java、C++、Python ではそれぞれ nullnullptrNone と表記します。
    • C、C++、Go、Rust などポインタをサポートする言語では、上記の「参照」は「ポインタ」に置き換えるべきです。

    以下のコードが示すように、連結リストノード ListNode は値のほかに、追加で 1 つの参照(ポインタ)を保持する必要があります。そのため、同じデータ量であれば、連結リストは配列より多くのメモリ空間を消費します。

    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":"

    連結リストの構築は 2 つの手順に分かれます。第 1 に各ノードオブジェクトを初期化し、第 2 にノード間の参照関係を構築します。初期化が完了したら、連結リストの先頭ノードから出発し、参照で 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%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%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%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%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

    配列全体は 1 つの変数であり、たとえば配列 nums には nums[0]nums[1] などの要素が含まれます。一方、連結リストは複数の独立したノードオブジェクトで構成されます。通常、先頭ノードを連結リストの代名詞として扱います。たとえば上記のコードの連結リストは n0 と表せます。

    ","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2","level":3,"title":"2.   ノードの挿入","text":"

    連結リストへのノード挿入は非常に簡単です。下図に示すように、隣り合う 2 つのノード n0n1 の間に新しいノード P を挿入したいとします。このとき 2 つのノードの参照(ポインタ)を変更するだけでよく、時間計算量は \\(O(1)\\) です。

    これに対して、配列に要素を挿入する時間計算量は \\(O(n)\\) であり、データ量が大きい場合の効率は低くなります。

    図 4-6   連結リストへのノード挿入例

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def insert(n0: ListNode, P: ListNode):\n    \"\"\"連結リストでノード n0 の後ろにノード P を挿入する\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
    linked_list.cpp
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.java
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.cs
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.go
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
    linked_list.swift
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
    linked_list.js
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.ts
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\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
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
    linked_list.rs
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\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
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.kt
    /* 連結リストでノード n0 の後ろにノード P を挿入する */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
    linked_list.rb
    ### 連結リストのノード n0 の後にノード _p を挿入 ###\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":"

    下図に示すように、連結リストでのノード削除も非常に簡単で、1 つのノードの参照(ポインタ)を変更するだけで済みます。

    なお、削除操作が完了した後もノード 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)\\) 時間でアクセスできます。これに対して連結リストでは、プログラムは先頭ノードから出発し、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-vs","level":2,"title":"4.2.2   配列 vs. 連結リスト","text":"

    次の表は、配列と連結リストの各種特徴と操作効率をまとめたものです。両者は互いに逆の格納戦略を採用しているため、各種性質や操作効率にも対照的な特徴が現れます。

    表 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":"

    下図に示すように、一般的な連結リストの種類は 3 つあります。

    • 単方向連結リスト:前述した通常の連結リストのことです。単方向連結リストのノードは、値と次のノードを指す参照の 2 つのデータを含みます。最初のノードを先頭ノード、最後のノードを末尾ノードと呼び、末尾ノードは空 None を指します。
    • 循環連結リスト:単方向連結リストの末尾ノードを先頭ノードへ向けると(先頭と末尾をつなぐと)、循環連結リストが得られます。循環連結リストでは、任意のノードを先頭ノードとみなせます。
    • 双方向連結リスト:単方向連結リストと比べて、双方向連結リストは 2 方向の参照を記録します。双方向連結リストのノード定義には、後続ノード(次のノード)と前駆ノード(前のノード)を指す参照(ポインタ)が含まれます。単方向連結リストより柔軟で、2 方向に連結リストを走査できますが、そのぶん多くのメモリ空間を必要とします。
    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":"

    単方向連結リストは、スタック、キュー、ハッシュテーブル、グラフなどのデータ構造の実装によく用いられます。

    • スタックとキュー:挿入と削除の両方の操作を連結リストの一端で行うと、その性質は後入れ先出しとなり、スタックに対応します。挿入を連結リストの一端で行い、削除をもう一端で行うと、その性質は先入れ先出しとなり、キューに対応します。
    • ハッシュテーブル:連鎖アドレス法はハッシュ衝突を解決する主流の方式の 1 つであり、この方式では、衝突したすべての要素が 1 つの連結リストに格納されます。
    • グラフ:隣接リストはグラフを表現する一般的な方法の 1 つであり、グラフの各頂点は 1 つの連結リストに関連付けられます。連結リスト内の各要素は、その頂点に接続されたほかの頂点を表します。

    双方向連結リストは、前後の要素をすばやく見つける必要がある場面でよく用いられます。

    • 高度なデータ構造:たとえば赤黒木や B 木では、ノードの親ノードへアクセスする必要があります。これは、ノード内に親ノードを指す参照を保持することで実現でき、双方向連結リストに似ています。
    • ブラウザ履歴:Web ブラウザでユーザーが進むボタンや戻るボタンをクリックしたとき、ブラウザはユーザーが訪れた前後のページを知る必要があります。双方向連結リストの性質によって、この操作は簡単になります。
    • LRU アルゴリズム:キャッシュ淘汰(LRU)アルゴリズムでは、最近最も使用されていないデータをすばやく見つける必要があり、さらにノードの高速な追加と削除も必要です。そのため、双方向連結リストが非常に適しています。

    循環連結リストは、オペレーティングシステムのリソーススケジューリングのように、周期的な操作が必要な場面でよく用いられます。

    • ラウンドロビン時間片スケジューリングアルゴリズム:オペレーティングシステムにおいて、ラウンドロビン時間片スケジューリングは一般的な CPU スケジューリングアルゴリズムであり、一連のプロセスを循環的に処理する必要があります。各プロセスには 1 つの時間片が割り当てられ、その時間片を使い切ると、CPU は次のプロセスへ切り替わります。この循環操作は、循環連結リストで実現できます。
    • データバッファ:一部のデータバッファ実装でも、循環連結リストが使われることがあります。たとえば音声・動画プレーヤーでは、データストリームを複数のバッファブロックに分割して循環連結リストへ格納し、シームレス再生を実現できます。
    ","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   リスト","text":"

    リスト(list)は抽象的なデータ構造の概念であり、要素の順序付き集合を表す。要素のアクセス、更新、追加、削除、走査などの操作をサポートし、利用者は容量制限の問題を考慮する必要がない。リストは連結リストまたは配列に基づいて実装できる。

    • 連結リストは本質的にリストと見なすことができ、要素の追加・削除・参照・更新をサポートし、柔軟に動的拡張できる。
    • 配列も要素の追加・削除・参照・更新をサポートするが、長さが不変であるため、長さ制限のあるリストとしか見なせない。

    配列でリストを実装する場合、長さが不変である性質によってリストの実用性が低下する。これは、通常は事前にどれだけのデータを格納する必要があるかを決められず、適切なリスト長を選びにくいためである。長さが小さすぎると利用要件を満たせない可能性が高く、大きすぎるとメモリ空間の浪費を招く。

    この問題を解決するために、動的配列(dynamic array)を用いてリストを実装できる。これは配列の各種利点を引き継ぎつつ、プログラム実行中に動的な拡張を行える。

    実際には、多くのプログラミング言語の標準ライブラリが提供するリストは動的配列に基づいて実装されている。たとえば、Python の list 、Java の ArrayList 、C++ の vector 、C# の List などである。以降の議論では、「リスト」と「動的配列」を同じ概念として扱う。

    ","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":"

    通常は「初期値なし」と「初期値あり」の 2 つの初期化方法を用いる。

    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// 初期値あり(配列の要素型は int[] のラッパークラスである Integer[] である必要があることに注意)\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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20%23%20%E6%97%A0%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%E8%AE%BF%E9%97%AE%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%0A%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%E5%B0%86%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%E6%9B%B4%E6%96%B0%E4%B8%BA%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)  # インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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:]...)...) // インデックス 3 に数値 6 を挿入\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) // インデックス 3 に数値 6 を挿入\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); // インデックス 3 に数値 6 を挿入\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); // インデックス 3 に数値 6 を挿入\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); // インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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);  // インデックス 3 に数値 6 を挿入\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) # インデックス 3 に数値 6 を挿入\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%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B8%85%E7%A9%BA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E5%B0%BE%E9%83%A8%E6%B7%BB%E5%8A%A0%E5%85%83%E7%B4%A0%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%E5%9C%A8%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.insert%283,%206%29%20%20%23%20%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0&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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%80%9A%E8%BF%87%E7%B4%A2%E5%BC%95%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%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%E7%9B%B4%E6%8E%A5%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%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
    # 2 つのリストを連結\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # リスト nums1 を nums の後ろに連結\n
    list.cpp
    /* 2 つのリストを連結 */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// リスト nums1 を nums の後ろに連結\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
    list.java
    /* 2 つのリストを連結 */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // リスト nums1 を nums の後ろに連結\n
    list.cs
    /* 2 つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // リスト nums1 を nums の後ろに連結\n
    list_test.go
    /* 2 つのリストを連結 */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // リスト nums1 を nums の後ろに連結\n
    list.swift
    /* 2 つのリストを連結 */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // リスト nums1 を nums の後ろに連結\n
    list.js
    /* 2 つのリストを連結 */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // リスト nums1 を nums の後ろに連結\n
    list.ts
    /* 2 つのリストを連結 */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // リスト nums1 を nums の後ろに連結\n
    list.dart
    /* 2 つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // リスト nums1 を nums の後ろに連結\n
    list.rs
    /* 2 つのリストを連結 */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
    list.c
    // C には組み込みの動的配列がない\n
    list.kt
    /* 2 つのリストを連結 */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // リスト nums1 を nums の後ろに連結\n
    list.rb
    # 2 つのリストを連結\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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8B%BC%E6%8E%A5%E4%B8%A4%E4%B8%AA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums1%20%3D%20%5B6,%208,%207,%2010,%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%20nums1%20%E6%8B%BC%E6%8E%A5%E5%88%B0%20nums%20%E4%B9%8B%E5%90%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/#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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8E%92%E5%BA%8F%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.sort%28%29%20%20%23%20%E6%8E%92%E5%BA%8F%E5%90%8E%EF%BC%8C%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E4%BB%8E%E5%B0%8F%E5%88%B0%E5%A4%A7%E6%8E%92%E5%88%97&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 などがある。それらの実装は比較的複雑で、初期容量や拡張倍率など各種パラメータの設定もよく考えられている。興味があればソースコードを参照して学べる。

    リストの動作原理への理解を深めるため、ここでは簡易版のリストを実装し、以下の 3 つの設計ポイントを含める。

    • 初期容量:妥当な配列の初期容量を選ぶ。この例では 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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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 以降の要素をすべて 1 つ後ろへずらす\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 より後の要素をすべて 1 つ前に移動する\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   コンピュータの記憶装置

    ハードディスク メモリ キャッシュ 用途 OS、プログラム、ファイルなどを長期保存 実行中のプログラムや処理中のデータを一時保存 頻繁にアクセスされるデータや命令を保存し、CPU のメモリアクセス回数を減らす 揮発性 電源断後もデータは失われない 電源断後にデータは失われる 電源断後にデータは失われる 容量 大きい、TB 級 小さい、GB 級 非常に小さい、MB 級 速度 遅い、数百〜数千 MB/s 速い、数十 GB/s 非常に速い、数十〜数百 GB/s 価格(人民元) 比較的安価、数角〜数元 / GB 比較的高価、数十〜数百元 / GB 非常に高価、CPU と一体で価格設定される

    コンピュータの記憶システムは、下図のようなピラミッド構造として捉えられます。ピラミッドの頂点に近い記憶装置ほど速度は速く、容量は小さく、コストは高くなります。この多層構造は偶然ではなく、コンピュータ科学者やエンジニアによる熟慮の末の設計です。

    • ハードディスクはメモリで置き換えにくい。まず、メモリ内のデータは電源断後に失われるため、長期保存には向きません。次に、メモリのコストはハードディスクの数十倍であり、消費者市場で広く普及しにくいという問題があります。
    • キャッシュは大容量と高速性を両立しにくい。L1、L2、L3 キャッシュの容量が段階的に増えるにつれて、物理サイズは大きくなり、CPU コアとの物理的距離も遠くなります。その結果、データ転送時間が増え、要素アクセスの遅延も大きくなります。現在の技術では、多層キャッシュ構造が容量、速度、コストの最適なバランスです。

    図 4-9   コンピュータの記憶システム

    Tip

    コンピュータの記憶階層は、速度、容量、コストの三者間にある巧妙なバランスを体現しています。実際、このようなトレードオフはあらゆる工業分野に広く存在しており、異なる利点と制約のあいだで最適な均衡点を見つけることが求められます。

    要するに、ハードディスクは大量データの長期保存に、メモリはプログラム実行中に処理しているデータの一時保存に、キャッシュは頻繁にアクセスされるデータや命令の保存に用いられ、プログラム実行効率を高めます。三者は協調して動作し、コンピュータシステムの高効率な運用を支えています。

    次の図に示すように、プログラム実行時にはデータがハードディスクからメモリへ読み込まれ、CPU の計算に使われます。キャッシュは CPU の一部と見なせ、メモリからデータを賢く読み込むことで、CPU に高速なデータ読み出しを提供し、プログラムの実行効率を大きく高め、低速なメモリへの依存を減らします。

    図 4-10   ハードディスク、メモリ、キャッシュ間のデータの流れ

    ","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)と呼び、この指標は通常、キャッシュ効率の評価に用いられます。

    できるだけ高い効率を実現するため、キャッシュは次のようなデータ読み込みの仕組みを採用しています。

    • キャッシュライン:キャッシュはデータを 1 バイト単位で保存・読み込みするのではなく、キャッシュライン単位で扱います。1 バイト単位の転送と比べて、キャッシュライン単位のほうが効率的です。
    • プリフェッチ機構:プロセッサはデータアクセスのパターン(たとえば順次アクセス、一定ステップ幅のスキップアクセスなど)を予測し、そのパターンに応じてデータをキャッシュへ読み込むことで、ヒット率を高めます。
    • 空間的局所性:あるデータがアクセスされた場合、その近傍のデータも近いうちにアクセスされる可能性があります。そのため、キャッシュはあるデータを読み込む際に、その周辺のデータもあわせて読み込み、ヒット率を高めます。
    • 時間的局所性:あるデータがアクセスされた場合、そのデータは近い将来に再びアクセスされる可能性が高いです。キャッシュはこの性質を利用し、最近アクセスしたデータを保持することでヒット率を高めます。

    実際には、配列と連結リストではキャッシュの利用効率が異なり、主に次の点に表れます。

    • 使用空間:連結リストの要素は配列要素より多くの空間を占めるため、キャッシュに収まる有効データ量は少なくなります。
    • キャッシュライン:連結リストのデータはメモリの各所に分散しており、キャッシュは「ライン単位で読み込む」ため、無効データまで読み込む割合が高くなります。
    • プリフェッチ機構:配列のほうが連結リストよりもデータアクセスのパターンを「予測しやすく」、システムが次に読み込まれるデータを推測しやすくなります。
    • 空間的局所性:配列はまとまったメモリ空間に格納されるため、読み込まれたデータの近くにあるデータも、まもなくアクセスされる可能性が高くなります。

    全体として、配列はより高いキャッシュヒット率を持つため、操作効率では通常、連結リストより優れています。このため、アルゴリズム問題を解く際には、配列ベースで実装されたデータ構造のほうが好まれることが多くなります。

    注意すべきなのは、**キャッシュ効率が高いからといって、配列があらゆる状況で連結リストより優れているとは限らない**という点です。実際にどのデータ構造を選ぶかは、具体的な要件に応じて決めるべきです。たとえば、配列と連結リストはいずれも「スタック」データ構造を実装できますが(次章で詳しく説明します)、適した場面は異なります。

    • アルゴリズム問題に取り組むときは、一般に配列ベースのスタックを選ぶ傾向があります。より高い操作効率とランダムアクセス能力を備えており、その代償は配列用に一定量のメモリを事前確保することだけです。
    • データ量が非常に大きく、動的性が高く、スタックの想定サイズを見積もりにくい場合は、連結リストベースのスタックのほうが適しています。連結リストなら大量のデータをメモリの異なる場所に分散して保存でき、配列拡張による追加コストも回避できます。
    ","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":"
    • 配列と連結リストは 2 種類の基本的なデータ構造であり、それぞれコンピュータメモリにおけるデータの 2 つの格納方式、すなわち連続領域への格納と分散領域への格納を表す。両者の特徴は相互補完的である。
    • 配列はランダムアクセスをサポートし、使用メモリも少ない。一方で、要素の挿入と削除の効率は低く、初期化後に長さを変更できない。
    • 連結リストは参照(ポインタ)を変更することでノードの挿入と削除を効率的に行え、長さも柔軟に調整できる。一方で、ノードへのアクセス効率は低く、メモリ使用量も多い。一般的な連結リストには単方向連結リスト、循環連結リスト、双方向連結リストがある。
    • リストは、追加・削除・検索・更新をサポートする順序付き要素集合であり、通常は動的配列に基づいて実装される。配列の利点を保ちながら、長さを柔軟に調整できる。
    • リストの登場により配列の実用性は大幅に高まったが、一部のメモリ領域が無駄になる可能性がある。
    • プログラムの実行時、データは主にメモリに格納される。配列はより高いメモリ空間効率を提供でき、連結リストはメモリ利用の面でより柔軟である。
    • キャッシュは、キャッシュライン、プリフェッチ機構、空間局所性と時間局所性といったデータ読み込み機構を通じて 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:なぜ配列では同じ型の要素が求められるのに、連結リストでは同じ型であることが強調されないのですか?

    連結リストはノードで構成され、ノード同士は参照(ポインタ)で接続されている。各ノードには intdoublestringobject など、異なる型のデータを格納できる。

    これに対して、配列要素は同じ型でなければならない。そうでなければ、オフセットを計算して対応する要素位置を取得できないからである。たとえば、配列に intlong の 2 種類が同時に含まれていて、各要素がそれぞれ 4 バイトと 8 バイトを占める場合、配列内に 2 種類の「要素長」が存在するため、次の式ではオフセットを計算できない。

    # 要素のメモリアドレス = 配列のメモリアドレス(先頭要素のメモリアドレス) + 要素長 * 要素インデックス\n

    Q:ノード P を削除した後、P.nextNone に設定する必要はありますか?

    P.next を変更しなくてもよい。この連結リストの観点では、先頭ノードから末尾ノードまでたどっても、もはや P に出会うことはない。つまり、ノード P はすでに連結リストから削除されており、この時点で P がどこを指していても、この連結リストには影響しない。

    データ構造とアルゴリズム(問題を解くとき)の観点では、切り離さなくても問題はなく、プログラムのロジックが正しいことを保証すればよい。標準ライブラリの観点では、切り離したほうがより安全で、ロジックも明確である。切り離さない場合、削除されたノードが適切に回収されなかったとすると、後続ノードのメモリ回収に影響する可能性がある。

    Q:連結リストでの挿入と削除の時間計算量は \\(O(1)\\) です。しかし、追加や削除の前には要素を探すのに \\(O(n)\\) の時間が必要です。では、なぜ時間計算量は \\(O(n)\\) ではないのですか?

    要素を先に探してから削除するのであれば、時間計算量が \\(O(n)\\) であるのは確かである。しかし、連結リストの \\(O(1)\\) での追加・削除という利点は、ほかの用途で生かせる。たとえば、両端キューは連結リストで実装するのに適しており、先頭ノードと末尾ノードを常に指すポインタ変数を維持すれば、各挿入・削除操作はどれも \\(O(1)\\) になる。

    Q:図「連結リストの定義と格納方式」で、薄青色のノードポインタ部分は 1 つのメモリアドレスを占めているのですか? それともノード値と半分ずつなのでしょうか?

    この模式図は定性的な表現にすぎず、定量的な表現は具体的な状況に応じて分析する必要がある。

    • ノード値が占める領域は型によって異なり、たとえば intlongdouble、インスタンスオブジェクトなどがある。
    • ポインタ変数が占めるメモリ空間の大きさは、使用する OS やコンパイル環境によって異なり、多くは 8 バイトまたは 4 バイトである。

    Q:リストの末尾への要素追加は常に \\(O(1)\\) ですか?

    要素を追加する際にリスト長を超える場合は、先にリストを拡張してから追加する必要がある。システムは新しいメモリ領域を確保し、元のリストの全要素をそこへ移動するため、このとき時間計算量は \\(O(n)\\) になる。

    Q:「リストの登場により配列の実用性は大きく向上したが、一部のメモリ空間が無駄になる可能性がある」というのは、容量、長さ、拡張倍率のような追加変数が占めるメモリのことですか?

    ここでいう空間の無駄には主に 2 つの意味がある。一方では、リストには初期長が設定されるが、必ずしもそれだけ必要とは限らない。もう一方では、頻繁な拡張を防ぐため、拡張時には通常ある係数、たとえば \\(\\times 1.5\\) を掛ける。このため、多くの空きスロットが生じ、通常それらを完全に埋めることはできない。

    Q:Python で n = [1, 2, 3] を初期化した後、この 3 つの要素のアドレスは連続しています。しかし m = [2, 1, 3] を初期化すると、各要素の id は連続しておらず、それぞれ n 内の同じ値と一致していることがわかります。これらの要素のアドレスが連続していないなら、m も配列なのですか?

    仮にリスト要素を連結リストのノード n = [n1, n2, n3, n4, n5] に置き換えたとしても、通常この 5 つのノードオブジェクトもメモリ上の各所に分散して格納される。それでも、与えられたリストインデックスに対して、私たちは依然として \\(O(1)\\) 時間でノードのメモリアドレスを取得し、対応するノードにアクセスできる。これは、配列に格納されているのがノードそのものではなく、ノードへの参照だからである。

    多くの言語と異なり、Python では数値もオブジェクトとしてラップされており、リストに格納されているのは数値そのものではなく、数値への参照である。そのため、2 つの配列内の同じ数値が同一の id を持つことがあり、しかもそれらの数値のメモリアドレスは連続している必要がない。

    Q:C++ STL の std::list はすでに双方向連結リストを実装していますが、アルゴリズム本ではあまり直接使われないようです。何か制約があるのでしょうか?

    一方では、私たちは多くの場合、アルゴリズムの実装に配列を好み、必要なときにだけ連結リストを使う。その主な理由は 2 つある。

    • 空間オーバーヘッド:各要素には 2 つの追加ポインタ(前の要素用と次の要素用)が必要なため、std::list は通常 std::vector より多くの空間を消費する。
    • キャッシュ非効率:データが連続して格納されていないため、std::list はキャッシュの利用効率が低い。一般には、std::vector のほうが性能がよい。

    もう一方では、連結リストを使う必要がある代表的な場面は主に二分木とグラフである。スタックやキューについては、連結リストではなく、たいてい言語が提供する stackqueue を使う。

    Q:res = [[0]] * n という操作で 2 次元リストを生成した場合、それぞれの [0] は独立していますか?

    独立していない。この 2 次元リストでは、すべての [0] は実際には同一オブジェクトへの参照である。そのうちの 1 つを変更すると、対応するすべての要素が一緒に変化することがわかる。

    2 次元リスト内の各 [0] を独立させたい場合は、res = [[0] for _ in range(n)] を使って実現できる。この方式の原理は、独立した [0] リストオブジェクトを \\(n\\) 個初期化していることにある。

    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 章   バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"章の内容","text":"
    • 13.1   バックトラッキングアルゴリズム
    • 13.2   全順列問題
    • 13.3   部分和問題
    • 13.4   n クイーン問題
    • 13.5   まとめ
    ","path":["第 13 章   バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   バックトラッキングアルゴリズム","text":"

    バックトラッキングアルゴリズム(backtracking algorithm)は、総当たりによって問題を解く手法です。その中核となる考え方は、初期状態から出発し、あり得るすべての解を力任せに探索し、正しい解に到達したらそれを記録し、解を見つけるか、考えられるすべての選択を試しても解が見つからなくなるまで続ける、というものです。

    バックトラッキングアルゴリズムでは、通常「深さ優先探索」を用いて解空間をたどります。「二分木」の章で述べたように、前順・中順・後順走査はいずれも深さ優先探索に属します。ここでは前順走査を使ってバックトラッキング問題を構成し、その仕組みを段階的に理解していきます。

    例題1

    1 本の二分木が与えられたとき、値が \\(7\\) のノードをすべて探索して記録し、そのノードのリストを返してください。

    この問題では、この木を前順走査し、現在のノードの値が \\(7\\) かどうかを判定します。該当する場合は、そのノードの値を結果リスト res に追加します。関連する処理は下図と次のコードのとおりです。

    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では、各ノードへの訪問が 1 回の「試行」に対応し、葉ノードを越えるか親ノードへ戻る return は「戻る」を表します。

    ここで強調しておきたいのは、**戻るとは関数の 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 から取り除き、**今回の試行前の状態を復元する**必要があります。

    次の図に示す過程を見ると、試行と戻るは「前進」と「取り消し」として理解できます。この 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":"

    複雑なバックトラッキング問題には、通常 1 つ以上の制約条件が含まれます。制約条件は多くの場合「枝刈り」に利用できます。

    例題3

    二分木の中で値が \\(7\\) のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。ただし、経路には値が \\(3\\) のノードを含めてはいけません。

    上の制約条件を満たすために、枝刈り操作を追加する必要があります。探索中に値が \\(3\\) のノードに出会った場合は、そこで早めに return し、それ以上探索を続けません。コードは次のとおりです。

    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   制約条件にもとづく枝刈り

    ","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 文は削除しなければなりません。次の図は、return 文を残す場合と削除する場合の探索過程を比較したものです。

    図 13-4   return を残す場合と削除する場合の探索過程の比較

    前順走査にもとづく実装と比べると、バックトラッキングアルゴリズムのフレームワークにもとづく実装はやや冗長に見えますが、汎用性に優れています。実際、多くのバックトラッキング問題はこのフレームワークで解けます。具体的な問題に応じて statechoices を定義し、各メソッドを実装すれば十分です。

    ","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   よく使われる用語","text":"

    アルゴリズム問題をより明確に分析するために、バックトラッキングでよく使われる用語の意味を整理し、例題3に対応する例を次の表にまとめます。

    表 13-1   よく使われるバックトラッキング用語

    用語 定義 例題3 解(solution) 問題の特定の条件を満たす答えであり、1 つまたは複数存在し得る 根ノードからノード \\(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":"

    バックトラッキングアルゴリズムの本質は深さ優先探索です。条件を満たす解を見つけるまで、あり得るすべての解を試します。この方法の利点は、考えられるすべての解を見つけられることであり、適切な枝刈りを行えば高い効率を発揮します。

    しかし、大規模または複雑な問題を扱う場合、バックトラッキングアルゴリズムの実行効率は受け入れがたいことがあります。

    • 時間:バックトラッキングアルゴリズムでは通常、状態空間のすべての可能性をたどる必要があり、時間計算量は指数時間や階乗時間に達することがあります。
    • 空間:再帰呼び出しの過程では現在の状態(たとえば経路や枝刈り用の補助変数など)を保持する必要があり、深さが大きいと空間使用量も大きくなります。

    それでもなお、バックトラッキングアルゴリズムは一部の探索問題や制約充足問題に対する最良の解法です。この種の問題では、どの選択が有効な解を生むかを事前に予測できないため、可能な選択肢をすべてたどる必要があります。このときの鍵は**いかに効率を最適化するか**であり、代表的な方法は 2 つあります。

    • 枝刈り:解が生じないことが確実な経路を探索しないことで、時間と空間を節約する。
    • ヒューリスティック探索:探索中に何らかの戦略や推定値を導入し、有効な解を生みやすい経路を優先的に探索する。
    ","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316","level":2,"title":"13.1.6   バックトラッキングの典型例題","text":"

    バックトラッキングアルゴリズムは、多くの探索問題、制約充足問題、組合せ最適化問題の解決に利用できます。

    探索問題:この種の問題の目標は、特定の条件を満たす解を見つけることです。

    • 全順列問題:ある集合が与えられたとき、考えられるすべての順列を求める。
    • 部分和問題:ある集合と目標和が与えられたとき、和が目標値となるすべての部分集合を見つける。
    • ハノイの塔問題:3 本の柱と大きさの異なる複数の円盤が与えられたとき、すべての円盤を 1 本の柱から別の柱へ移動する。ただし 1 回に 1 枚しか動かせず、大きい円盤を小さい円盤の上に置いてはならない。

    制約充足問題:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。

    • \\(n\\) クイーン問題:\\(n \\times n\\) の盤面に \\(n\\) 個のクイーンを配置し、互いに攻撃し合わないようにする。
    • 数独:\\(9 \\times 9\\) のグリッドに数字 \\(1\\) ~ \\(9\\) を入れ、各行・各列・各 \\(3 \\times 3\\) の小区画で数字が重複しないようにする。
    • グラフ彩色問題:無向グラフが与えられたとき、隣接する頂点が同じ色にならないように、できるだけ少ない色で各頂点を彩色する。

    組合せ最適化問題:この種の問題の目標は、組合せ空間の中で条件を満たす最適解を見つけることです。

    • 0-1 ナップサック問題:複数の品物とナップサックが与えられ、各品物には価値と重さがある。ナップサック容量の範囲内で総価値が最大になるように品物を選ぶ。
    • 巡回セールスマン問題:グラフ内のある頂点から出発し、他のすべての頂点をちょうど 1 回ずつ訪れて出発点へ戻るときの最短経路を求める。
    • 最大クリーク問題:無向グラフが与えられたとき、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。

    多くの組合せ最適化問題では、バックトラッキングは最適な解法ではない点に注意してください。

    • 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\\) サイズの盤面が与えられたとき、すべてのクイーンが互いに攻撃し合わない配置を求めます。

    下図に示すように、\\(n = 4\\) のとき、2 つの解を見つけることができます。バックトラッキングの観点から見ると、\\(n \\times n\\) サイズの盤面には合計 \\(n^2\\) 個のマスがあり、これがすべての選択肢 choices を与えます。クイーンを 1 つずつ配置していく過程で、盤面の状態は絶えず変化し、その各時点の盤面が状態 state です。

    図 13-15   4 クイーン問題の解

    下図は本問題の 3 つの制約条件を示しています。複数のクイーンは同じ行、同じ列、同じ対角線上に置けません。なお、対角線には主対角線 \\ と副対角線 / の 2 種類があります。

    図 13-16   n クイーン問題の制約条件

    ","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1","level":3,"title":"1.   行ごとの配置戦略","text":"

    クイーンの数と盤面の行数はいずれも \\(n\\) なので、次の推論を容易に得られます:盤面の各行にはクイーンを 1 つだけ配置できます。

    つまり、行ごとの配置戦略を採用できます:最初の行から始めて、各行に 1 つのクイーンを配置し、最後の行まで進みます。

    下図は 4 クイーン問題における行ごとの配置過程を示しています。図の大きさの都合上、下図では 1 行目における検索分岐の 1 つだけを展開し、列制約と対角線制約を満たさない案はすべて枝刈りしています。

    図 13-17   行ごとの配置戦略

    本質的には、行ごとの配置戦略は枝刈りとして機能します。これにより、同じ行に複数のクイーンが現れるすべての探索分岐を回避できます。

    ","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2","level":3,"title":"2.   列と対角線の枝刈り","text":"

    列制約を満たすために、長さ \\(n\\) のブール配列 cols を用いて、各列にクイーンがあるかどうかを記録できます。配置を決めるたびに、cols を使って既存のクイーンがある列を枝刈りし、バックトラッキングの中で cols の状態を動的に更新します。

    Tip

    注意として、行列の原点は左上にあり、行インデックスは上から下へ、列インデックスは左から右へ増加します。

    では、対角線制約はどのように扱えばよいのでしょうか。盤面上のあるマスの行列インデックスを \\((row, col)\\) とし、行列内のある主対角線を選ぶと、その対角線上のすべてのマスで行インデックスから列インデックスを引いた値が等しいことが分かります。つまり、主対角線上のすべてのマスでは \\(row - col\\) が一定値になります。

    つまり、2 つのマスが \\(row_1 - col_1 = row_2 - col_2\\) を満たすなら、それらは必ず同じ主対角線上にあります。この性質を利用して、下図の配列 diags1 により、各主対角線にクイーンがあるかどうかを記録できます。

    同様に、副対角線上のすべてのマスでは \\(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\\) であり、配列 diags1diags2 の長さもともに \\(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\\) 回配置し、列制約を考慮すると、1 行目から最終行までの選択肢はそれぞれ \\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 個となるため、時間計算量は \\(O(n!)\\) です。解を記録する際には、行列 state をコピーして res に追加する必要があり、このコピー操作には \\(O(n^2)\\) 時間を要します。したがって、全体の時間計算量は \\(O(n! \\cdot n^2)\\) です。実際には、対角線制約による枝刈りも探索空間を大きく縮小できるため、探索効率はしばしば上記の時間計算量より良くなります。

    配列 state は \\(O(n^2)\\) の空間を使用し、配列 colsdiags1diags2 はいずれも \\(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   全順列の例

    入力配列 すべての順列 \\([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]\\) が得られます。戻る操作は 1 つの選択を取り消し、その後で別の選択を試し続けることを表します。

    バックトラッキングコードの観点では、候補集合 choices は入力配列中のすべての要素であり、状態 state は現時点までに選ばれた要素です。各要素は 1 回しか選べないことに注意してください。したがって state 内の要素はすべて一意でなければなりません。

    下図のように、探索過程は再帰木として展開できます。木の各ノードは現在の状態 state を表します。根ノードから始めて 3 ラウンドの選択を経て葉ノードに到達し、各葉ノードが 1 つの順列に対応します。

    図 13-5   全順列の再帰木

    ","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1","level":3,"title":"1.   重複選択の枝刈り","text":"

    各要素が 1 回しか選ばれないようにするため、ブール配列 selected の導入を考えます。ここで selected[i]choices[i] がすでに選ばれているかどうかを表し、これに基づいて次の枝刈りを行います。

    • 選択 choice[i] を行った後、selected[i] を \\(\\text{True}\\) に設定し、その要素が選択済みであることを表します。
    • 選択肢リスト choices を走査するとき、すでに選ばれたノードはすべてスキップします。これが枝刈りです。

    下図のように、1 回目に 1、2 回目に 3、3 回目に 2 を選ぶとします。このとき 2 回目では要素 1 の分岐を、3 回目では要素 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]\\) だと仮定します。2 つの重複する要素 \\(1\\) を区別しやすくするため、2 つ目の \\(1\\) を \\(\\hat{1}\\) と記します。

    下図のように、上述の方法で生成される順列の半分は重複しています。

    図 13-7   重複した順列

    では、重複した順列をどのように取り除けばよいのでしょうか。最も直接的なのは、ハッシュ集合を用いて順列結果をそのまま重複排除する方法です。しかしこのやり方は十分に洗練されていません。なぜなら、重複順列を生成する探索分岐はそもそも不要であり、事前に見つけて枝刈りすべきだからです。そうすることで、アルゴリズム効率をさらに高められます。

    ","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1_1","level":3,"title":"1.   等しい要素の枝刈り","text":"

    下図を見ると、1 回目のラウンドでは \\(1\\) を選ぶことと \\(\\hat{1}\\) を選ぶことは等価であり、これら 2 つの選択の下で生成される順列はすべて重複します。したがって \\(\\hat{1}\\) を枝刈りすべきです。

    同様に、1 回目で \\(2\\) を選んだ後では、2 回目のラウンドにおける \\(1\\) と \\(\\hat{1}\\) も重複分岐を生むため、2 回目の \\(\\hat{1}\\) も枝刈りすべきです。

    本質的には、各ラウンドの選択において、等しい複数の要素が 1 回しか選ばれないようにすることが目標です。

    図 13-8   重複順列の枝刈り

    ","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2_1","level":3,"title":"2.   コード実装","text":"

    前問のコードを土台として、各ラウンドの選択でハッシュ集合 duplicated を 1 つ用意し、そのラウンドですでに試した要素を記録して、重複要素を枝刈りすることを考えます。

    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)\\) 空間を使用します。同時刻に存在する duplicated は最大で \\(n\\) 個であり、\\(O(n^2)\\) 空間を要します。したがって空間計算量は \\(O(n^2)\\) です。

    ","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3-2","level":3,"title":"3.   2 種類の枝刈りの比較","text":"

    selectedduplicated はどちらも枝刈りに用いられますが、目的は異なる点に注意してください。

    • 重複選択の枝刈り:探索全体を通して selected は 1 つだけです。これは現在の状態にどの要素が含まれているかを記録し、ある要素が state に重複して現れるのを防ぎます。
    • 等しい要素の枝刈り:各ラウンドの選択、すなわち各回の backtrack 呼び出しには duplicated が含まれます。これはそのラウンドの走査(for ループ)でどの要素がすでに選ばれたかを記録し、等しい要素が 1 回しか選ばれないことを保証します。

    下図は、2 つの枝刈り条件が有効になる範囲を示しています。木の各ノードは 1 つの選択を表し、根ノードから葉ノードまでの経路上の各ノードが 1 つの順列を構成することに注意してください。

    図 13-9   2 種類の枝刈り条件の作用範囲

    ","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\\}\\) です。次の 2 点に注意してください。

    • 入力集合内の要素は何度でも繰り返し選択できます。
    • 部分集合では要素の順序を区別しません。例えば \\(\\{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; // 解の個数を 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]\\) が含まれています。

    これは、探索過程では選択順を区別する一方で、部分集合では選択順を区別しないためです。次の図のように、先に \\(4\\) を選んでから \\(5\\) を選ぶ場合と、先に \\(5\\) を選んでから \\(4\\) を選ぶ場合は別の分岐ですが、対応する部分集合は同じです。

    図 13-10   部分集合探索と境界超過の枝刈り

    重複する部分集合を取り除くために、**直接的な方法として結果リストの重複を除去する**ことが考えられます。しかし、この方法は効率が低く、その理由は次の 2 点です。

    • 配列要素が多い場合、特に target が大きい場合には、探索過程で大量の重複部分集合が生成されます。
    • 部分集合(配列)同士の違いを比較するのは非常に時間がかかり、まず配列をソートし、その後に各要素を比較する必要があります。
    ","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2","level":3,"title":"2.   重複部分集合の枝刈り","text":"

    **探索過程で枝刈りを行って重複を除去する**ことを考えます。次の図を観察すると、重複部分集合は配列要素を異なる順序で選択したときに生じます。例えば次のような状況です。

    1. 1 回目と 2 回目でそれぞれ \\(3\\) と \\(4\\) を選ぶと、これら 2 要素を含むすべての部分集合、すなわち \\([3, 4, \\dots]\\) が生成されます。
    2. その後、1 回目で \\(4\\) を選んだ場合、**2 回目では \\(3\\) をスキップすべき**です。というのも、この選択で生成される部分集合 \\([4, 3, \\dots]\\) は、手順 1. で生成された部分集合と完全に重複するからです。

    探索過程では、各階層の選択は左から右へ順に試されるため、右側にある分岐ほど多く枝刈りされます。

    1. 最初の 2 回で \\(3\\) と \\(5\\) を選ぶと、部分集合 \\([3, 5, \\dots]\\) が生成されます。
    2. 最初の 2 回で \\(4\\) と \\(5\\) を選ぶと、部分集合 \\([4, 5, \\dots]\\) が生成されます。
    3. もし 1 回目で \\(5\\) を選ぶなら、**2 回目では \\(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\\) を満たし、部分集合の一意性が保証されます。

    これに加えて、コードには次の 2 つの最適化も施しています。

    • 探索を始める前に、まず配列 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
    コードの可視化

    全画面で見る >

    次の図は、配列 \\([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 に等しくなるすべての組合せを見つけてください。与えられた配列には重複要素が含まれる可能性があり、各要素は 1 回しか選択できません。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。

    前問と比べると、この問題の入力配列には重複要素が含まれる可能性があります。そのため、新たな問題が生じます。例えば、配列 \\([4, \\hat{4}, 5]\\) と目標値 \\(9\\) が与えられると、既存コードの出力は \\([4, 5], [\\hat{4}, 5]\\) となり、重複部分集合が現れます。

    この重複が生じる原因は、同じ値の要素があるラウンドで複数回選ばれてしまうことにあります。次の図では、1 回目には 3 つの選択肢があり、そのうち 2 つはどちらも \\(4\\) です。これにより 2 本の重複した探索分岐が生じ、重複部分集合が出力されます。同様に、2 回目の 2 つの \\(4\\) も重複部分集合を生みます。

    図 13-13   等しい要素によって生じる重複部分集合

    ","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1_1","level":3,"title":"1.   等しい要素の枝刈り","text":"

    この問題を解決するには、各ラウンドで等しい要素が 1 回しか選ばれないように制限する必要があります。実装方法は巧妙です。配列はすでにソートされているため、等しい要素は必ず隣り合っています。したがって、あるラウンドの選択で現在の要素が左隣の要素と等しいなら、それはすでに選ばれたことを意味するので、その要素を直接スキップします。

    同時に、**この問題では各配列要素を 1 回しか選択できない**という制約もあります。幸い、この制約も変数 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
    コードの可視化

    全画面で見る >

    次の図は、配列 \\([4, 4, 5]\\) と目標値 \\(9\\) に対するバックトラッキング過程を示しており、全部で 4 種類の枝刈り操作が含まれています。図とコードコメントを対応させながら、探索全体の流れと、各枝刈り操作がどのように機能するかを理解してください。

    図 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":"
    • バックトラッキングアルゴリズムの本質は全探索法であり、解空間を深さ優先で走査することで条件を満たす解を探索します。探索の過程で条件を満たす解に出会ったら記録し、すべての解を見つけるか探索が完了するまで続けます。
    • バックトラッキングアルゴリズムの探索過程は、試行と戻るという 2 つの部分から成ります。深さ優先探索によってさまざまな選択を試し、制約条件を満たさない状況に遭遇した場合は直前の選択を取り消して前の状態に戻り、ほかの選択を引き続き試します。試行と戻るは互いに逆方向の操作です。
    • バックトラッキング問題には通常複数の制約条件が含まれており、それらを枝刈りに利用できます。枝刈りによって不要な探索分岐を早期に打ち切り、探索効率を大幅に高められます。
    • バックトラッキングアルゴリズムは主に探索問題と制約充足問題の解決に用いられます。組合せ最適化問題もバックトラッキングで解けますが、より高効率またはより適した解法が存在することが少なくありません。
    • 全順列問題の目的は、与えられた集合要素のすべての可能な並べ方を探索することです。各要素が選択済みかどうかを配列で記録し、同じ要素を重複して選ぶ探索分岐を刈り取ることで、各要素が 1 度だけ選ばれるようにします。
    • 全順列問題では、集合内に重複要素があると最終結果にも重複した順列が現れます。各ラウンドで等しい要素は 1 回しか選べないように制約する必要があり、通常はハッシュ集合を用いて実現します。
    • 部分和問題の目標は、与えられた集合の中から和が目標値となるすべての部分集合を見つけることです。集合では要素順序を区別しませんが、探索過程では順序違いの結果も出力されるため、重複部分集合が生じます。そこで、バックトラッキング前にデータをソートし、各ラウンドの走査開始位置を示す変数を設定することで、重複部分集合を生成する探索分岐を枝刈りします。
    • 部分和問題では、配列中の等しい要素が重複集合を生みます。配列がソート済みであるという前提を利用し、隣接要素が等しいかどうかを判定して枝刈りすることで、等しい要素が各ラウンドで 1 回しか選ばれないようにします。
    • \\(n\\) クイーン問題の目的は、\\(n \\times n\\) の盤面に \\(n\\) 個のクイーンを配置する方法を見つけることであり、どの 2 つのクイーンも互いに攻撃できないことが条件です。この問題の制約には行制約、列制約、主対角線制約、副対角線制約があります。行制約を満たすため、行ごとに配置する戦略を採用し、各行に 1 個のクイーンを置くことを保証します。
    • 列制約と対角線制約の扱い方は似ています。列制約については、各列にクイーンが存在するかどうかを配列で記録し、選択したマスが有効かどうかを判定します。対角線制約については、主対角線と副対角線それぞれにクイーンが存在するかを 2 つの配列で記録します。難点は、同じ主対角線または副対角線上にあるマスが満たす行列インデックスの規則を見つけることにあります。
    ","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q:バックトラッキングと再帰の関係はどのように理解すればよいですか?

    全体として見ると、バックトラッキングは「アルゴリズム戦略」の一種であり、再帰はむしろ「道具」に近いものです。

    • バックトラッキングアルゴリズムは通常、再帰に基づいて実装されます。ただし、バックトラッキングは再帰の応用場面の 1 つであり、探索問題における再帰の応用です。
    • 再帰の構造は「部分問題への分解」という問題解決パラダイムを表しており、分割統治、バックトラッキング、動的計画法(メモ化再帰)などの問題によく用いられます。
    ","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章   計算量解析","text":"

    Abstract

    計算量解析は、広大なアルゴリズム宇宙における時空の案内人のようなものです。

    それは、時間と空間という二つの次元で私たちをより深く探求へ導き、より洗練された解決策を見つけ出します。

    ","path":["第 2 章   計算量解析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"章の内容","text":"
    • 2.1   アルゴリズム効率の評価
    • 2.2   反復と再帰
    • 2.3   時間計算量
    • 2.4   空間計算量
    • 2.5   まとめ
    ","path":["第 2 章   計算量解析"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   反復と再帰","text":"

    アルゴリズムでは、ある処理を繰り返し実行することがよくあり、これは複雑度解析と密接に関係しています。そのため、時間計算量と空間計算量を紹介する前に、まずプログラム内で反復実行を実現する方法、つまり 2 つの基本的な制御構造である反復と再帰について見ていきます。

    ","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 つで、反復回数があらかじめ分かっている場合に適しています。

    次の関数は for ループを用いて \\(1 + 2 + \\dots + n\\) の総和を計算しており、その結果は変数 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 ループも反復を実現する方法の 1 つです。while ループでは、各反復のたびにまず条件を確認し、条件が真であれば実行を続け、そうでなければループを終了します。

    次に、while ループを使って \\(1 + 2 + \\dots + n\\) の総和を求めてみましょう。

    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\\) が各反復で 2 回更新されており、このようなケースは for ループではあまり扱いやすくありません。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
    def while_loop_ii(n: int) -> int:\n    \"\"\"while ループ(2回更新)\"\"\"\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2回更新) */\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 ループ(2 回更新)###\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":"

    1 つのループ構造の中に別のループ構造を入れ子にできます。以下では 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\\) と「二次関係」にあります。

    さらにネストしたループを追加することもできます。ネストが 1 段増えるたびに「次元が 1 つ上がる」ことになり、時間計算量は「三次関係」「四次関係」へと高くなっていきます。

    ","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222","level":2,"title":"2.2.2   再帰","text":"

    再帰(recursion)は、関数が自分自身を呼び出すことで問題を解決するアルゴリズム戦略です。主に 2 つの段階から成ります。

    1. 再帰呼び出し:プログラムは自分自身をより深く呼び出し続け、通常はより小さい、またはより単純化された引数を渡し、「終了条件」に達するまで進みます。
    2. 復帰: 「終了条件」が満たされると、プログラムは最も深い再帰関数から 1 層ずつ戻り、各層の結果をまとめていきます。

    実装の観点から見ると、再帰コードは主に 3 つの要素から成ります。

    1. 終了条件:いつ再帰呼び出しから復帰へ切り替わるかを決めます。
    2. 再帰呼び出し:再帰呼び出しに対応し、関数が自分自身を呼び出します。通常はより小さい、またはより単純化された引数を入力します。
    3. 結果の返却:復帰に対応し、現在の再帰レベルの結果を 1 つ上の層へ返します。

    次のコードを見ると、関数 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 つのパラダイムを表しています。

    • 反復:「ボトムアップ」で問題を解決します。最も基本的な手順から始め、それらを繰り返したり積み上げたりして、処理が完了するまで進めます。
    • 再帰:「トップダウン」で問題を解決します。元の問題をより小さな部分問題に分解し、それらの部分問題は元の問題と同じ形を持ちます。さらに部分問題をより小さな部分問題へと分解し、基本ケースに達したところで停止します(基本ケースの解は既知です)。

    前述の総和関数を例に、問題を \\(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 つの結果が生じます。

    • 関数のコンテキストデータは「スタックフレーム領域」と呼ばれるメモリ領域に保存され、関数が戻るまで解放されません。したがって、再帰は通常、反復より多くのメモリ空間を消費します。
    • 再帰による関数呼び出しには追加のオーバーヘッドが発生します。そのため再帰は通常、ループより時間効率が低くなります。

    次の図のように、終了条件が発動する前には、まだ戻っていない再帰関数が同時に \\(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 つ上の階層の関数へ戻った後も、引き続きコードを実行する必要があるため、システムは 1 つ上の呼び出しのコンテキストを保存しておく必要があります。
    • 末尾再帰:再帰呼び出しが関数の返却前の最後の操作であるため、1 つ上の階層へ戻った後に他の処理を続ける必要がなく、システムは 1 つ上の関数のコンテキストを保存する必要がありません。

    \\(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   末尾再帰の過程

    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)\\) とすると、次の 2 つが容易に分かります。

    • 数列の最初の 2 項は \\(f(1) = 0\\) と \\(f(2) = 1\\) です。
    • 数列中の各項は直前の 2 項の和であり、すなわち \\(f(n) = f(n - 1) + f(n - 2)\\) です。

    漸化式に従って再帰呼び出しを行い、最初の 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 回の再帰呼び出しを行っています。これは 1 回の呼び出しから 2 つの呼び出し分岐が生じることを意味します。次の図のように、この再帰呼び出しを繰り返していくと、最終的に深さ \\(n\\) の再帰木(recursion tree)が生成されます。

    図 2-6   フィボナッチ数列の再帰木

    本質的に見ると、再帰は「問題をより小さな部分問題へ分解する」という思考パラダイムを体現しており、この分割統治の戦略は非常に重要です。

    • アルゴリズムの観点では、探索、ソート、バックトラッキング、分割統治、動的計画法など、多くの重要な戦略が直接または間接にこの考え方を用いています。
    • データ構造の観点では、再帰は連結リスト、木、グラフに関する問題の処理に本質的に適しており、これらは分割統治の考え方で分析しやすいからです。
    ","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223","level":2,"title":"2.2.3   両者の比較","text":"

    以上をまとめると、次の表のように、反復と再帰は実装、性能、適用性の面で違いがあります。

    表 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
    コードの可視化

    全画面で見る >

    上のコードを見ると、再帰を反復へ変換すると、コードはより複雑になります。反復と再帰は多くの場合に相互変換できますが、常にそうする価値があるとは限りません。理由は次の 2 点です。

    • 変換後のコードは理解しにくくなり、可読性が下がる可能性があります。
    • 複雑な問題によっては、システムの呼び出しスタックの振る舞いを模擬すること自体が非常に難しい場合があります。

    要するに、反復を選ぶか再帰を選ぶかは、対象となる問題の性質によって決まります。実際のプログラミングでは、両者の長所と短所を見極め、状況に応じて適切な方法を選ぶことが重要です。

    ","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   アルゴリズム効率の評価","text":"

    アルゴリズム設計では、次の 2 つのレベルの目標を順に追求します。

    1. 問題の解法を見つける:アルゴリズムは、定められた入力範囲内で問題の正しい解を確実に求められる必要があります。
    2. 最適な解法を追求する:同じ問題に対して複数の解法が存在する場合があり、私たちはできるだけ効率的なアルゴリズムを見つけたいと考えます。

    つまり、問題を解けることを前提として、アルゴリズム効率はその良し悪しを測る主要な評価指標となっており、次の 2 つの観点を含みます。

    • 時間効率:アルゴリズムの実行時間の長さ。
    • 空間効率:アルゴリズムが使用するメモリ空間の大きさ。

    簡単に言えば、**私たちの目標は「高速で省メモリ」なデータ構造とアルゴリズムを設計すること**です。そして、アルゴリズム効率を効果的に評価することは非常に重要です。そうすることで初めて、さまざまなアルゴリズムを比較し、さらにアルゴリズム設計と最適化の過程を導けるからです。

    効率の評価方法は主に 2 種類に分けられます。実測と理論的な見積もりです。

    ","path":["第 2 章   計算量解析","2.1   アルゴリズム効率の評価"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211","level":2,"title":"2.1.1   実測","text":"

    いまアルゴリズム A とアルゴリズム B があり、どちらも同じ問題を解けるとします。この 2 つのアルゴリズムの効率を比較する必要がある場合、最も直接的な方法は 1 台のコンピュータで両者を実行し、その実行時間とメモリ使用量を監視して記録することです。この評価方法は実際の状況を反映できますが、大きな制約もあります。

    一方では、**テスト環境による干渉要因を排除しにくい**という問題があります。ハードウェア構成はアルゴリズムの性能に影響します。たとえば、並列度の高いアルゴリズムはマルチコア 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)と呼ばれ、略して計算量解析といいます。

    計算量解析は、アルゴリズムの実行に必要な時間資源と空間資源が入力データ規模とどのような関係にあるかを表します。これは、入力データ規模が増加するにつれて、アルゴリズムの実行に必要な時間と空間がどのように増加するかという傾向を記述するものです。この定義はややわかりにくいので、次の 3 つのポイントに分けて理解できます。

    • 「時間資源と空間資源」は、それぞれ時間計算量(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   アルゴリズムで使用される関連空間

    関連するコードを以下に示します。

    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() は実行中に未返却の recur() が同時に \\(n\\) 個存在するため、\\(O(n)\\) のスタックフレーム空間を占有します。
    ","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243","level":2,"title":"2.4.3   よくある型","text":"

    入力データサイズを \\(n\\) とすると、以下の図はよくある空間計算量の型を低い順から高い順に示しています。

    \\[ \\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
    コードの可視化

    全画面で見る >

    以下の図に示すように、この関数の再帰の深さは \\(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
    コードの可視化

    全画面で見る >

    以下の図に示すように、この関数の再帰の深さは \\(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":"

    アルゴリズム効率の評価

    • 時間効率と空間効率は、アルゴリズムの良し悪しを測る二つの主要な評価指標です。
    • 実測によってアルゴリズム効率を評価できますが、テスト環境の影響を排除しにくく、多くの計算資源も消費します。
    • 複雑度分析は実測の欠点を補い、分析結果はすべての実行プラットフォームに適用でき、データ規模ごとの効率も明らかにできます。

    時間計算量

    • 時間計算量は、アルゴリズムの実行時間がデータ量の増加に伴ってどう変化するかを測るためのものであり、効率評価に有効です。ただし、入力データ量が小さい場合や時間計算量が同じ場合などには、効率の優劣を正確に比較できないことがあります。
    • 最悪時間計算量はビッグオー記法 \\(O\\) で表され、関数の漸近上界に対応し、\\(n\\) が正の無限大に近づくときの操作回数 \\(T(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)はオブジェクトに関連付けられ、それを呼び出すオブジェクトが暗黙的に渡され、クラスのインスタンスに含まれるデータを操作できます。

    以下では、いくつかの一般的なプログラミング言語を例に説明します。

    • C 言語は手続き型プログラミング言語であり、オブジェクト指向の概念がないため、関数しかありません。ただし、構造体(struct)を作成してオブジェクト指向プログラミングを模倣でき、構造体に関連付けられた関数は、他のプログラミング言語におけるメソッドに相当します。
    • Java と C# はオブジェクト指向のプログラミング言語であり、コードブロック(メソッド)は通常あるクラスの一部です。静的メソッドの振る舞いは関数に似ており、クラスに束縛され、特定のインスタンス変数にはアクセスできません。
    • C++ と Python は、手続き型プログラミング(関数)にもオブジェクト指向プログラミング(メソッド)にも対応しています。

    Q:「一般的な空間計算量の種類」の図が表しているのは、使用空間の絶対量ですか?

    いいえ。この図が示しているのは空間計算量であり、表しているのは増加傾向であって、使用空間の絶対量ではありません。

    \\(n = 8\\) と仮定すると、各曲線の値が対応する関数と一致していないように見えるかもしれません。これは、各曲線に定数項が含まれており、値の範囲を視覚的に見やすい範囲へ圧縮しているためです。

    実際には、各手法の「定数項」の複雑度がどれほどか通常は分からないため、一般に複雑度だけを根拠に \\(n = 8\\) 以下で最適解を選ぶことはできません。ただし、\\(n = 8^5\\) であれば選びやすく、このときは増加傾向がすでに支配的になっています。

    Q 実際の利用場面に応じて、時間(または空間)を犠牲にしてアルゴリズムを設計することはありますか?

    実際の応用では、多くの場合、空間を犠牲にして時間を得る選択をします。たとえばデータベースのインデックスでは、通常 B+ 木やハッシュインデックスを構築し、大量のメモリ空間を使う代わりに、\\(O(\\log n)\\) あるいは \\(O(1)\\) の高速な検索を実現します。

    空間資源が貴重な場面では、時間を犠牲にして空間を得ることもあります。たとえば組み込み開発では、デバイスのメモリが非常に貴重なため、エンジニアはハッシュテーブルの使用をやめ、配列による順次探索を選んでメモリ使用量を節約することがあります。その代償として探索は遅くなります。

    ","path":["第 2 章   計算量解析","2.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   時間計算量","text":"

    実行時間はアルゴリズムの効率を直感的かつ正確に反映します。あるコードの実行時間を正確に見積もりたい場合、どのようにすればよいでしょうか?

    1. 実行プラットフォームを特定する。ハードウェア構成、プログラミング言語、システム環境などが含まれ、これらの要因はいずれもコードの実行効率に影響します。
    2. 各種計算操作に必要な実行時間を評価する。例えば加算 + には 1 ns 、乗算 * には 10 ns 、出力 print() には 5 ns などが必要です。
    3. コード中のすべての計算操作を数える。そして各操作の実行時間を合計することで、実行時間を得ます。

    例えば次のコードでは、入力データサイズを \\(n\\) とします:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # ある実行プラットフォーム上で\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # n 回ループ\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
    // ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // n 回ループ\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // n 回ループ\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // n 回ループ\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // n 回ループ\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // n 回ループ\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
    // ある実行プラットフォーム上で\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // n 回ループ\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
    // ある実行プラットフォーム上で\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // n 回ループ\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
    # ある実行プラットフォーム上で\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # n 回ループ\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

    上記の方法に基づくと、アルゴリズムの実行時間は \\((6n + 12)\\) ns になります:

    \\[ 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\\) とし、3 つのアルゴリズム ABC を考えます:

    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

    以下の図は、上記 3 つのアルゴリズム関数の時間計算量を示しています。

    • アルゴリズム A には出力操作が \\(1\\) 回しかなく、実行時間は \\(n\\) が大きくなっても増加しません。このアルゴリズムの時間計算量を「定数階」と呼びます。
    • アルゴリズム B の出力操作は \\(n\\) 回ループする必要があり、実行時間は \\(n\\) の増加に対して線形に増加します。このアルゴリズムの時間計算量は「線形階」と呼ばれます。
    • アルゴリズム C の出力操作は \\(1000000\\) 回ループする必要があり、実行時間は長いものの、入力データサイズ \\(n\\) とは無関係です。したがって C の時間計算量は A と同じく、依然として「定数階」です。

    図 2-7   アルゴリズム A、B、C の時間増加傾向

    アルゴリズムの実行時間を直接数える方法と比べて、時間計算量分析にはどのような特徴があるでしょうか?

    • 時間計算量はアルゴリズム効率を有効に評価できます。例えばアルゴリズム B の実行時間は線形に増加するため、\\(n > 1\\) ではアルゴリズム A より遅く、\\(n > 1000000\\) ではアルゴリズム C より遅くなります。実際、入力データサイズ \\(n\\) が十分に大きければ、「定数階」のアルゴリズムは必ず「線形階」のアルゴリズムより優れます。これが実行時間の増加傾向の意味です。
    • 時間計算量の見積もり方法はより簡潔です。実行プラットフォームや計算操作の種類は、アルゴリズム実行時間の増加傾向とは無関係です。そのため時間計算量分析では、すべての計算操作の実行時間を同じ「単位時間」とみなしてよく、「計算操作の実行時間を数える」作業を「計算操作の個数を数える」作業へ簡略化できます。これにより見積もりの難易度は大きく下がります。
    • 時間計算量には一定の限界もあります。例えばアルゴリズム AC の時間計算量は同じでも、実際の実行時間には大きな差があります。同様に、アルゴリズム B の時間計算量は C より高いものの、入力データサイズ \\(n\\) が小さい場合にはアルゴリズム B のほうが明らかに優れます。このような場合、時間計算量だけでアルゴリズム効率の高低を判断するのは難しいことがあります。もっとも、こうした問題があっても、複雑度分析は依然としてアルゴリズム効率を評価する最も有効で一般的な方法です。
    ","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)\\) と表します。この数学記号はビッグ \\(O\\) 記法(big-\\(O\\) notation)と呼ばれ、関数 \\(T(n)\\) の漸近上界(asymptotic upper bound)を表します。

    時間計算量の分析は本質的に「操作回数 \\(T(n)\\)」の漸近上界を求めることであり、明確な数学的定義があります。

    関数の漸近上界

    正の実数 \\(c\\) と実数 \\(n_0\\) が存在し、すべての \\(n > n_0\\) について \\(T(n) \\leq c \\cdot f(n)\\) が成り立つならば、\\(f(n)\\) は \\(T(n)\\) の漸近上界の 1 つであるとみなせます。これを \\(T(n) = O(f(n))\\) と記します。

    下図のように、漸近上界を求めるとは関数 \\(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)\\) をどのように決めればよいのでしょうか。大きく 2 段階あります。まず操作回数を数え、その後で漸近上界を判断します。

    ","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-1","level":3,"title":"1.   第 1 ステップ:操作回数を数える","text":"

    コードについては、上から下へ 1 行ずつ数えれば十分です。しかし、前述の \\(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\\) が無限大に近づくとき、最高次の項が支配的となり、他の項の影響は無視できるからです。

    以下の表はその例です。いくつか極端な値を入れているのは、「係数では次数は変わらない」という結論を強調するためです。\\(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\\) とすると、よくある時間計算量の種類は次図のとおりです(小さい順に並べています)。

    \\[ \\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\\) は入力データの型に応じて具体的に定める必要がある**ということです。例えば 1 つ目の例では変数 \\(n\\) が入力データサイズであり、2 つ目の例では配列長 \\(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
    コードの可視化

    全画面で見る >

    以下の図は、定数階・線形階・平方階の 3 種類の時間計算量を比較したものです。

    図 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":"

    生物学における「細胞分裂」は指数階増加の典型例です。初期状態では細胞が \\(1\\) 個あり、1 回分裂すると \\(2\\) 個、2 回分裂すると \\(4\\) 個となり、以下同様に、\\(n\\) 回分裂すると \\(2^n\\) 個の細胞になります。

    以下の図とコードは細胞分裂の過程を模擬したもので、時間計算量は \\(O(2^n)\\) です。なお、入力の \\(n\\) は分裂回数を表し、戻り値 count は総分裂回数を表します。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
    def exponential(n: int) -> int:\n    \"\"\"指数時間(ループ実装)\"\"\"\n    count = 0\n    base = 1\n    # 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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  // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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    // 細胞は各ラウンドで 2 つに分裂し、数列 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  # 細胞は各ラウンドで 2 つに分裂し、数列 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   指数階の時間計算量

    実際のアルゴリズムでも、指数階は再帰関数によく現れます。例えば次のコードでは、再帰的に 2 つへ分岐し、\\(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  # 細胞は各ラウンドで 2 つに分裂し、数列 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\\) の逆関数になります。

    以下の図とコードは、「各ラウンドで半分になる」過程を模擬したもので、時間計算量は \\(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  # 細胞は各ラウンドで 2 つに分裂し、数列 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  # 細胞は各ラウンドで 2 つに分裂し、数列 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
    コードの可視化

    全画面で見る >

    対数階は分割統治に基づくアルゴリズムによく現れ、「1 つを複数に分ける」「複雑なものを単純化する」という考え方を体現しています。増加は緩やかで、定数階に次いで理想的な時間計算量です。

    \\(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":"

    線形対数階は入れ子ループによく現れ、2 層のループの時間計算量はそれぞれ \\(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
    コードの可視化

    全画面で見る >

    下図は線形対数階がどのように生じるかを示しています。二分木の各層の操作総数はすべて \\(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 \\]

    階乗は通常、再帰で実装されます。以下の図とコードのように、第 1 層では \\(n\\) 個に分岐し、第 2 層では \\(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    # 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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    // 1個から 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  // 1個から 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    // 1個から 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    // 1個から 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  # 1個から 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":"

    アルゴリズムの時間効率は固定ではなく、入力データの分布に左右されることが多いです。長さ \\(n\\) の配列 nums を考えます。nums は \\(1\\) から \\(n\\) までの数字で構成され、各数字は 1 回だけ現れます。ただし要素の順序はランダムにシャッフルされており、目標は要素 \\(1\\) のインデックスを返すことです。ここから次の結論が得られます。

    • nums = [?, ?, ..., 1]、つまり末尾の要素が \\(1\\) の場合は、配列全体を最後まで走査する必要があり、最悪時間計算量 \\(O(n)\\) になります。
    • nums = [1, ?, ?, ...]、つまり先頭要素が \\(1\\) の場合は、配列がどれだけ長くてもそれ以上走査する必要がなく、最良時間計算量 \\(\\Omega(1)\\) になります。

    「最悪時間計算量」は関数の漸近上界に対応し、ビッグ \\(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    \"\"\"配列 nums 内で数値 1 のインデックスを探す\"\"\"\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    // システム時刻を使って乱数シードを生成する\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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/* 配列 nums 内で数値 1 のインデックスを探す */\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### 配列 nums 内の数値 1 のインデックスを探す ###\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 章   データ構造"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"章の内容","text":"
    • 3.1   データ構造の分類
    • 3.2   基本データ型
    • 3.3   数値エンコーディング *
    • 3.4   文字エンコーディング *
    • 3.5   まとめ
    ","path":["第 3 章   データ構造"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   基本データ型","text":"

    コンピュータ内のデータについて考えるとき、テキスト、画像、動画、音声、3D モデルなど、さまざまな形態を思い浮かべます。これらのデータの構成形式はそれぞれ異なりますが、いずれも各種の基本データ型によって成り立っています。

    **基本データ型は CPU が直接演算できる型**であり、アルゴリズムの中で直接使われます。主なものは次のとおりです。

    • 整数型 byteshortintlong
    • 浮動小数点数型 floatdouble ,小数を表すために使います。
    • 文字型 char ,各言語の文字、句読点、さらには絵文字などを表すために使います。
    • 真偽値型 bool ,真か偽かの判定を表すために使います。

    基本データ型はコンピュータ内で 2 進数の形で格納されます。1 つの二進桁は \\(1\\) ビットです。現代のほとんどのオペレーティングシステムでは、\\(1\\) バイト(byte)は \\(8\\) ビット(bit)で構成されます。

    基本データ型の値域は、その型が占める領域の大きさによって決まります。以下では Java を例に取ります。

    • 整数型 byte は \\(1\\) バイト = \\(8\\) ビットを占め、\\(2^{8}\\) 個の数を表せます。
    • 整数型 int は \\(4\\) バイト = \\(32\\) ビットを占め、\\(2^{32}\\) 個の数を表せます。

    下表は、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 型はなく、1 文字は実際には長さ 1 の文字列 str です。
    • C と C++ では基本データ型の大きさは明確に規定されておらず、実装やプラットフォームによって異なります。上表は LP64 データモデル に従っており、Linux や macOS を含む Unix 系 64 ビット OS で用いられています。
    • char の大きさは C と C++ では 1 バイトですが、多くのプログラミング言語では採用する文字エンコーディング方式によって決まります。詳しくは「文字エンコーディング」の章を参照してください。
    • 真偽値を表すのに必要なのは 1 ビット(\\(0\\) または \\(1\\))だけですが、メモリ上では通常 1 バイトとして格納されます。これは、現代のコンピュータ CPU が通常 1 バイトを最小のアドレス指定可能なメモリ単位としているためです。

    では、基本データ型とデータ構造の間にはどのような関係があるのでしょうか。データ構造とは、コンピュータ内でデータを組織し格納する方法のことです。この言葉で主役なのは「データ」ではなく「構造」です。

    「数字の並び」を表したいなら、自然に配列の使用を思い浮かべるでしょう。これは、配列の線形構造が数字どうしの隣接関係や順序関係を表せるからです。しかし、格納する内容が整数 int なのか、小数 float なのか、文字 char なのかは、「データ構造」とは関係ありません。

    言い換えると、基本データ型はデータの「内容の型」を提供し、データ構造はデータの「組織方法」を提供します。たとえば次のコードでは、同じデータ構造(配列)を使って intfloatcharbool など異なる基本データ型を格納・表現しています。

    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%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%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 ビットの二進数(1 バイトの下位 7 ビット)で 1 文字を表し、最大で 128 種類の異なる文字を表現できます。下図のように、ASCII コードには英字の大文字と小文字、数字 0 ~ 9、いくつかの句読点、そしていくつかの制御文字(改行やタブなど)が含まれます。

    図 3-6   ASCII コード

    しかし、ASCII コードで表現できるのは英語だけです。コンピュータのグローバル化に伴い、より多くの言語を表せる EASCII 文字セットが生まれました。これは ASCII の 7 ビットを 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 コードでも多くの言語に必要な文字数を満たせない**ことに気づきました。たとえば漢字は 10 万字近くあり、日常的に使うものだけでも数千字あります。中国国家標準総局は 1980 年に GB2312 文字セットを公開し、6763 字の漢字を収録して、漢字のコンピュータ処理の基本的な需要を満たしました。

    しかし、GB2312 では一部の珍しい字や繁体字を扱えません。GBK 文字セットは GB2312 を基に拡張されたもので、合計 21886 字の漢字を収録しています。GBK のエンコーディング方式では、ASCII 文字は 1 バイト、漢字は 2 バイトで表されます。

    ","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode","level":2,"title":"3.4.3   Unicode 文字セット","text":"

    コンピュータ技術が急速に発展するにつれて、文字セットと符号化規格は百花繚乱の状態となり、それに伴って多くの問題も生じました。一方では、これらの文字セットは通常、特定の言語の文字しか定義しておらず、多言語環境では正常に動作できませんでした。もう一方では、同じ言語にも複数の文字セット規格が存在し、2 台のコンピュータが異なる符号化規格を使っていると、情報伝達の際に文字化けが発生しました。

    当時の研究者たちはこう考えました。十分に完全な文字セットを打ち出して、世界中のあらゆる言語と記号をそこに収録すれば、多言語環境や文字化けの問題を解決できるのではないか。この発想に後押しされて、大規模で包括的な文字セット Unicode が誕生しました。

    Unicode の中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理・表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。

    Unicode は汎用文字セットとして、本質的には各文字に固有の「コードポイント」(文字番号)を割り当てており、その範囲は U+0000 から U+10FFFF までで、統一された文字番号空間を構成しています。しかし、Unicode はそれらのコードポイントをコンピュータ内でどのように保存するかまでは規定していません。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか。

    この問題に対して、**すべての文字を固定長の符号として保存する**という直接的な解決策があります。下図のように、「Hello」の各文字は 1 バイト、「アルゴリズム」の各文字は 2 バイトを占めます。上位ビットを 0 で埋めることで、「Hello アルゴリズム」のすべての文字を 2 バイト長にエンコードできます。こうすれば、システムは 2 バイトごとに 1 文字を解析して、この語句の内容を復元できます。

    図 3-7   Unicode エンコーディングの例

    しかし ASCII コードはすでに、英語の符号化には 1 バイトで十分であることを示しています。上記の方式を採用すると、英語のテキストが占める空間は ASCII エンコーディング時の 2 倍になり、メモリ空間の浪費が大きくなります。そのため、より効率的な 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 文字を 1 〜 4 バイトで表し、文字の複雑さに応じて長さが変わります。ASCII 文字は 1 バイト、ラテン文字とギリシャ文字は 2 バイト、一般的な漢字は 3 バイト、そのほかの一部の珍しい文字は 4 バイト必要です。

    UTF-8 の符号化規則はそれほど複雑ではなく、次の 2 つのケースに分けられます。

    • 長さ 1 バイトの文字では、最上位ビットを \\(0\\) にし、残りの 7 ビットを Unicode コードポイントに設定します。ここで注意すべきなのは、ASCII 文字が Unicode 文字セットの先頭 128 個のコードポイントを占めていることです。つまり、UTF-8 エンコーディングは ASCII コードと下位互換性があります。このため、UTF-8 を使って古い ASCII コードのテキストを解析できます。
    • 長さ \\(n\\) バイトの文字(ただし \\(n > 1\\))では、先頭バイトの上位 \\(n\\) ビットをすべて \\(1\\) にし、第 \\(n + 1\\) ビットを \\(0\\) に設定します。2 バイト目以降では、各バイトの上位 2 ビットをいずれも \\(10\\) にし、残りのすべてのビットで文字の Unicode コードポイントを埋めます。

    下図は「Helloアルゴリズム」に対応する UTF-8 エンコーディングを示しています。観察すると、上位 \\(n\\) ビットがすべて \\(1\\) に設定されているため、システムは先頭から連続する \\(1\\) の個数を読むことで、その文字の長さが \\(n\\) であると解析できます。

    では、なぜ残りのすべてのバイトの上位 2 ビットを \\(10\\) にするのでしょうか。実は、この \\(10\\) は検査用の印として機能します。システムが誤ったバイト位置からテキストを解析し始めたとしても、バイト先頭の \\(10\\) によって異常を素早く判定できます。

    この \\(10\\) を検査用の印とする理由は、UTF-8 の符号化規則では上位 2 ビットが \\(10\\) になる文字は存在しないからです。この結論は背理法で証明できます。ある文字の上位 2 ビットが \\(10\\) だと仮定すると、その文字の長さは \\(1\\) であり、ASCII コードに対応することになります。しかし ASCII コードの最上位ビットは \\(0\\) であるはずなので、仮定と矛盾します。

    図 3-8   UTF-8 エンコーディングの例

    UTF-8 以外にも、一般的なエンコーディング方式として次の 2 つがあります。

    • UTF-16 エンコーディング:1 文字を 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\\) 文字を見つけるには文字列の先頭から第 \\(i\\) 文字まで走査する必要があり、\\(O(n)\\) の時間がかかります。
    • 文字数の計算:ランダムアクセスと同様に、UTF-16 で符号化された文字列の長さを計算するのも \\(O(1)\\) の操作です。しかし、UTF-8 で符号化された文字列の長さを計算するには、文字列全体を走査する必要があります。
    • 文字列操作:UTF-16 で符号化された文字列では、多くの文字列操作(分割、連結、挿入、削除など)をより簡単に行えます。UTF-8 で符号化された文字列では、これらの操作を行う際に、無効な UTF-8 エンコーディングを生じさせないための追加計算が通常必要になります。

    実際、プログラミング言語における文字エンコーディング方式の設計は、とても興味深い話題であり、多くの要因が関わっています。

    • Java の String 型は UTF-16 エンコーディングを使用し、各文字は 2 バイトを占めます。これは Java 言語の設計当初、人々が 16 ビットあればあらゆる文字を表現するのに十分だと考えていたためです。しかし、これは誤った判断でした。その後 Unicode 規格は 16 ビットを超える範囲へ拡張されたため、現在の Java では 1 文字が 16 ビット値の組(「サロゲートペア」)で表されることがあります。
    • JavaScript と TypeScript の文字列が UTF-16 エンコーディングを使う理由も Java と似ています。1995 年に Netscape 社が初めて JavaScript 言語を公開した当時、Unicode はまだ発展初期にあり、16 ビットの符号化で十分すべての Unicode 文字を表せると考えられていました。
    • C# が UTF-16 エンコーディングを使う主な理由は、.NET プラットフォームが Microsoft によって設計され、Microsoft の多くの技術(Windows オペレーティングシステムを含む)で UTF-16 エンコーディングが広く使われているためです。

    以上のプログラミング言語は文字数を過小評価していたため、16 ビットを超える長さの Unicode 文字を表すために「サロゲートペア」を採用せざるを得ませんでした。これはやむを得ない妥協策です。一方では、サロゲートペアを含む文字列では、1 文字が 2 バイトまたは 4 バイトを占める可能性があり、固定長エンコーディングの利点が失われます。もう一方では、サロゲートペアの処理には追加のコードが必要となり、プログラミングの複雑さとデバッグの難しさが増します。

    こうした理由から、一部のプログラミング言語では別のエンコーディング方式が採用されました。

    • Python の str は Unicode エンコーディングを使用し、柔軟な文字列表現を採用しています。保存される文字の長さは、その文字列中で最大の Unicode コードポイントに依存します。文字列がすべて ASCII 文字であれば各文字は 1 バイト、ASCII の範囲を超える文字があってもすべてが基本多言語面(BMP)内であれば各文字は 2 バイト、BMP を超える文字があれば各文字は 4 バイトを占めます。
    • Go 言語の string 型は内部で UTF-8 エンコーディングを使用します。Go 言語には単一の Unicode コードポイントを表す rune 型も用意されています。
    • Rust 言語の strString 型は内部で UTF-8 エンコーディングを使用します。Rust にも単一の Unicode コードポイントを表す char 型があります。

    注意すべきなのは、ここまでの議論はすべて、プログラミング言語内での文字列の保存方法についてであり、**文字列をファイルに保存したりネットワークで転送したりする方法とは別の問題である**ということです。ファイル保存やネットワーク転送では、通常、互換性と空間効率を最適化するために文字列を UTF-8 形式にエンコードします。

    ","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   データ構造の分類","text":"

    代表的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフがあり、これらは「論理構造」と「物理構造」の 2 つの観点から分類できます。

    ","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311","level":2,"title":"3.1.1   論理構造:線形と非線形","text":"

    論理構造はデータ要素間の論理的な関係を示します。配列と連結リストでは、データは一定の順序で並び、データ間の線形関係を表します。一方、木ではデータは上から下へ階層的に並び、「祖先」と「子孫」の派生関係を示します。グラフはノードと辺で構成され、複雑なネットワーク関係を反映します。

    以下の図に示すように、論理構造は「線形」と「非線形」の 2 つに大別できます。線形構造は比較的直感的で、データが論理関係において線形に並ぶことを指します。非線形構造はその逆で、非線形に配置されます。

    • 線形データ構造:配列、連結リスト、スタック、キュー、ハッシュテーブルであり、要素間は 1 対 1 の順序関係です。
    • 非線形データ構造:木、ヒープ、グラフ、ハッシュテーブル。

    非線形データ構造は、さらに木構造と網状構造に分けられます。

    • 木構造:木、ヒープ、ハッシュテーブルであり、要素間は 1 対多の関係です。
    • 網状構造:グラフであり、要素間は多対多の関係です。

    図 3-1   線形データ構造と非線形データ構造

    ","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312","level":2,"title":"3.1.2   物理構造:連続と分散","text":"

    アルゴリズムのプログラムが実行されるとき、処理中のデータは主にメモリに格納されます。下図はコンピュータのメモリモジュールを示しており、各黒い四角はそれぞれ 1 つのメモリ空間を表しています。メモリは巨大な Excel の表のようなものだと考えることができ、各セルには一定量のデータを格納できます。

    システムはメモリアドレスを通じて目的の位置にあるデータへアクセスします。下図に示すように、コンピュータは特定の規則に従って表内の各セルに番号を割り当て、各メモリ空間が一意のメモリアドレスを持つようにします。これらのアドレスがあれば、プログラムはメモリ内のデータにアクセスできます。

    図 3-2   メモリモジュール、メモリ空間、メモリアドレス

    Tip

    補足すると、メモリを Excel の表にたとえるのは単純化した比喩であり、実際のメモリの動作機構はより複雑で、アドレス空間、メモリ管理、キャッシュ機構、仮想メモリ、物理メモリなどの概念が関わります。

    メモリはすべてのプログラムで共有される資源であり、あるメモリ領域が 1 つのプログラムに占有されると、通常は他のプログラムが同時に利用できません。したがって、データ構造とアルゴリズムの設計では、メモリ資源は重要な考慮要素です。たとえば、アルゴリズムが使用するメモリ使用量のピークは、システムに残っている空きメモリを超えてはなりません。大きな連続メモリ領域が不足している場合、選択するデータ構造は分散したメモリ空間に格納できる必要があります。

    下図に示すように、物理構造はデータがコンピュータメモリ内にどのように格納されるかを表します。これは連続空間への格納(配列)と分散空間への格納(連結リスト)に分けられます。物理構造は低レベルでデータのアクセス、更新、追加、削除などの操作方法を決定し、2 種類の物理構造は時間効率と空間効率の面で相補的な特徴を持ちます。

    図 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-1-2","level":2,"title":"3.3.1   符号付き絶対値表現、1 の補数、2 の補数","text":"

    前節の表を見ると、すべての整数型で表せる負数の個数は正数より 1 つ多く、たとえば byte の値域は \\([-128, 127]\\) です。この現象は直感に反するように見えますが、その背景には符号付き絶対値表現、1 の補数、2 の補数に関する知識があります。

    まず押さえておくべきなのは、**数値はコンピュータ内で「2 の補数」の形で保存される**ということです。その理由を説明する前に、まずはこの 3 つの定義を示します。

    • 符号付き絶対値表現:数値の二進表現の最上位ビットを符号ビットとみなし、\\(0\\) は正数、\\(1\\) は負数を表し、残りのビットが数値の値を表します。
    • 1 の補数:正数の 1 の補数は符号付き絶対値表現と同じで、負数の 1 の補数は符号ビットを除くすべてのビットを反転したものです。
    • 2 の補数:正数の 2 の補数は符号付き絶対値表現と同じで、負数の 2 の補数は 1 の補数に \\(1\\) を加えたものです。

    下図は、符号付き絶対値表現、1 の補数、2 の補数の変換方法を示しています。

    図 3-4   符号付き絶対値表現、1 の補数、2 の補数の相互変換

    符号付き絶対値表現(sign-magnitude)は最も直感的ですが、いくつかの制約があります。まず、負数の符号付き絶対値表現はそのまま演算に使えません。たとえば符号付き絶対値表現で \\(1 + (-2)\\) を計算すると、結果は \\(-3\\) になってしまい、これは明らかに誤りです。

    \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

    この問題を解決するために、コンピュータには1 の補数(1's complement)が導入されました。まず符号付き絶対値表現を 1 の補数に変換し、1 の補数で \\(1 + (-2)\\) を計算してから、結果を 1 の補数から符号付き絶対値表現へ戻すと、正しい結果 \\(-1\\) が得られます。

    \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(符号付き絶対値表現)} + 1000 \\; 0010 \\; \\text{(符号付き絶対値表現)} \\newline & = 0000 \\; 0001 \\; \\text{(1 の補数)} + 1111 \\; 1101 \\; \\text{(1 の補数)} \\newline & = 1111 \\; 1110 \\; \\text{(1 の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(符号付き絶対値表現)} \\newline & \\rightarrow -1 \\end{aligned} \\]

    一方、数値 0 の符号付き絶対値表現には \\(+0\\) と \\(-0\\) の 2 つの表し方があります。つまり、数値 0 に対して異なる 2 つの二進コードが対応しており、これは曖昧さの原因になります。たとえば条件判定で正のゼロと負のゼロを区別しないと、誤った判定結果になる可能性があります。また、この曖昧さを解消しようとすると追加の判定処理が必要になり、計算効率が下がるおそれがあります。

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

    符号付き絶対値表現と同様に、1 の補数にも正負のゼロの曖昧さがあります。そこでコンピュータはさらに2 の補数(2's complement)を導入しました。まずは負のゼロについて、符号付き絶対値表現、1 の補数、2 の補数の変換を見てみましょう。

    \\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(符号付き絶対値表現)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(1 の補数)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(2 の補数)} \\newline \\end{aligned} \\]

    負のゼロの 1 の補数に \\(1\\) を加えると桁上がりが発生しますが、byte 型の長さは 8 ビットしかないため、第 9 ビットへあふれた \\(1\\) は捨てられます。つまり、負のゼロの 2 の補数は \\(0000 \\; 0000\\) であり、正のゼロの 2 の補数と同じです。そのため、2 の補数表現ではゼロは 1 つしか存在せず、正負のゼロの曖昧さは解消されます。

    最後にもう 1 つ疑問が残ります。byte 型の値域は \\([-128, 127]\\) ですが、余分にある負数 \\(-128\\) はどのように得られるのでしょうか。区間 \\([-127, +127]\\) にあるすべての整数には、それぞれ対応する符号付き絶対値表現、1 の補数、2 の補数があり、符号付き絶対値表現と 2 の補数の間は相互に変換できます。

    しかし、2 の補数 \\(1000 \\; 0000\\) だけは例外で、対応する符号付き絶対値表現を持ちません。変換規則に従うと、この 2 の補数に対応する符号付き絶対値表現は \\(0000 \\; 0000\\) になります。これは明らかに矛盾しています。なぜなら、この符号付き絶対値表現は数値 \\(0\\) を表し、その 2 の補数は自分自身であるはずだからです。コンピュータでは、この特別な 2 の補数 \\(1000 \\; 0000\\) を \\(-128\\) と定めています。実際、2 の補数での \\((-1) + (-127)\\) の計算結果はちょうど \\(-128\\) になります。

    \\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(符号付き絶対値表現)} + 1000 \\; 0001 \\; \\text{(符号付き絶対値表現)} \\newline & = 1000 \\; 0000 \\; \\text{(1 の補数)} + 1111 \\; 1110 \\; \\text{(1 の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(2 の補数)} + 1111 \\; 1111 \\; \\text{(2 の補数)} \\newline & = 1000 \\; 0000 \\; \\text{(2 の補数)} \\newline & \\rightarrow -128 \\end{aligned} \\]

    すでにお気づきかもしれませんが、上の計算はすべて加算です。これは重要な事実を示しています。**コンピュータ内部のハードウェア回路は、主として加算を基準に設計されている**のです。なぜなら、加算はほかの演算(乗算、除算、減算など)に比べてハードウェアで実装しやすく、並列化もしやすく、演算速度も速いからです。

    ただし、これはコンピュータが加算しかできないという意味ではありません。加算といくつかの基本的な論理演算を組み合わせることで、コンピュータはさまざまな数学演算を実現できます。たとえば減算 \\(a - b\\) は加算 \\(a + (-b)\\) に変換できますし、乗算や除算も繰り返しの加算または減算に変換できます。

    これで、コンピュータが 2 の補数を使う理由をまとめられます。2 の補数表現に基づけば、コンピュータは同じ回路と操作で正数と負数の加算を扱うことができ、減算専用の特別なハードウェア回路を設計する必要がなく、正負のゼロの曖昧さも特別に処理しなくて済みます。これにより、ハードウェア設計は大幅に簡略化され、演算効率も向上します。

    2 の補数の設計は非常に巧妙ですが、紙幅の都合上ここまでにします。興味のある読者は、さらに深く調べてみてください。

    ","path":["第 3 章   データ構造","3.3   数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332","level":2,"title":"3.3.2   浮動小数点数のエンコーディング","text":"

    注意深い人なら気づくかもしれません。intfloat はどちらも長さが 4 バイトで同じなのに、なぜ float の値域は int よりはるかに広いのでしょうか。これはかなり直感に反します。というのも、float は小数を表す必要があるので、本来なら値域は狭くなるはずだからです。

    実際には、これは浮動小数点数 float が異なる表現方法を採用しているためです。32 ビット長の二進数を次のように表します。

    \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

    IEEE 754 標準によれば、32-bit 長の float は次の 3 つの部分から構成されます。

    • 符号部 \\(\\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   IEEE 754 標準における float の計算例

    上図を見ると、例として \\(\\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 は値が大きくなるほど、隣り合う 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}\\) です。

    倍精度 doublefloat と同様の表現方法を採用しているため、ここでは詳述しません。

    ","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":"
    • データ構造は、論理構造と物理構造という 2 つの観点から分類できます。論理構造はデータ要素間の論理的関係を記述し、物理構造はデータのコンピュータメモリ上での格納方法を記述します。
    • 代表的な論理構造には、線形、木構造、網状構造などがあります。通常、論理構造に基づいてデータ構造を線形(配列、連結リスト、スタック、キュー)と非線形(木、グラフ、ヒープ)の 2 種類に分類します。ハッシュテーブルの実装には、線形データ構造と非線形データ構造が同時に含まれる場合があります。
    • プログラムの実行時、データはコンピュータメモリに格納されます。各メモリ空間には対応するメモリアドレスがあり、プログラムはそれらのメモリアドレスを通じてデータにアクセスします。
    • 物理構造は主に連続領域への格納(配列)と分散領域への格納(連結リスト)に分けられます。すべてのデータ構造は、配列、連結リスト、またはその両方の組み合わせによって実装されます。
    • コンピュータにおける基本データ型には、整数 byteshortintlong、浮動小数点数 floatdouble、文字 char、真偽値 bool があります。これらの値域は、使用する記憶領域の大きさと表現方式によって決まります。
    • 符号付き絶対値表現、1 の補数、2 の補数は、コンピュータで数値を符号化する 3 つの方法であり、相互に変換できます。整数の符号付き絶対値表現では最上位ビットが符号ビットで、残りのビットが数値の値です。
    • 整数はコンピュータ内では 2 の補数の形式で格納されます。2 の補数表現では、コンピュータは正数と負数の加算を同じように扱うことができ、減算のために特別なハードウェア回路を別途設計する必要がなく、さらに正負のゼロが重複する問題もありません。
    • 浮動小数点数の符号化は、1 ビットの符号部、8 ビットの指数部、23 ビットの仮数部で構成されます。指数部があるため、浮動小数点数の値域は整数よりはるかに広くなりますが、その代償として精度が犠牲になります。
    • ASCII コードは最も早く登場した英字文字集合で、長さは 1 バイト、収録文字数は 127 です。GBK 文字集合はよく使われる中国語文字集合で、2 万字以上の漢字を収録しています。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:なぜハッシュテーブルには線形データ構造と非線形データ構造が同時に含まれるのですか?

    ハッシュテーブルの基盤は配列であり、ハッシュ衝突を解決するために「チェイン法」(後続の「ハッシュ衝突」の章で説明します)を使うことがあります。配列内の各バケットは 1 つの連結リストを指し、その連結リストの長さがある閾値を超えると、木(通常は赤黒木)に変換されることもあります。

    格納の観点から見ると、ハッシュテーブルの基盤は配列であり、各バケットスロットには値が入ることもあれば、連結リストや木が入ることもあります。したがって、ハッシュテーブルには線形データ構造(配列、連結リスト)と非線形データ構造(木)が同時に含まれる場合があります。

    Q:char 型の長さは 1 バイトですか?

    char 型の長さは、プログラミング言語が採用する符号化方式によって決まります。たとえば、Java、JavaScript、TypeScript、C# はいずれも UTF-16 符号化(Unicode コードポイントを保持)を採用しているため、char 型の長さは 2 バイトです。

    Q:配列ベースで実装されたデータ構造を「静的データ構造」と呼ぶのは曖昧ではありませんか? スタックも push や pop などの操作ができ、これらの操作はどれも「動的」です。

    スタックは確かに動的なデータ操作を実現できますが、データ構造自体は依然として「静的」(長さが不変)です。配列ベースのデータ構造でも要素を動的に追加または削除できますが、その容量は固定です。データ量が事前に確保した大きさを超えた場合は、より大きな新しい配列を作成し、古い配列の内容を新しい配列にコピーする必要があります。

    Q:スタック(キュー)を構築するときにサイズを指定していないのに、なぜそれらは「静的データ構造」なのですか?

    高水準プログラミング言語では、スタック(キュー)の初期容量を人手で指定する必要はなく、この作業はクラス内部で自動的に行われます。たとえば、Java の ArrayList の初期容量は通常 10 です。また、容量拡張も自動的に実装されています。詳しくは後続の「リスト」の章を参照してください。

    Q:符号付き絶対値表現から 2 の補数への変換方法は「先にビット反転してから 1 を加える」ですが、2 の補数から符号付き絶対値表現への変換は逆演算である「先に 1 を引いてからビット反転する」べきなのに、同じく「先にビット反転してから 1 を加える」でも求められます。これはなぜですか?

    これは、符号付き絶対値表現と 2 の補数の相互変換が、実際には「補数」を計算する過程だからです。まず補数の定義を示します。\\(a + b = c\\) とすると、\\(a\\) を \\(b\\) から \\(c\\) への補数と呼び、逆に \\(b\\) も \\(a\\) から \\(c\\) への補数と呼びます。

    長さ \\(n = 4\\) ビットの 2 進数 \\(0010\\) が与えられたとします。この数を符号付き絶対値表現(符号ビットは考慮しない)とみなすと、その 2 の補数は「先にビット反転してから 1 を加える」ことで得られます。

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

    ここで、符号付き絶対値表現と 2 の補数の和は \\(0010 + 1110 = 10000\\) となります。つまり、2 の補数 \\(1110\\) は符号付き絶対値表現 \\(0010\\) から \\(10000\\) への「補数」です。これは、上記の「先にビット反転してから 1 を加える」が、実際には \\(10000\\) への補数を計算する過程であることを意味します。

    では、2 の補数 \\(1110\\) から \\(10000\\) への「補数」はいくつでしょうか。これもやはり「先にビット反転してから 1 を加える」ことで求められます。

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

    言い換えると、符号付き絶対値表現と 2 の補数は互いに相手から \\(10000\\) への「補数」なので、「符号付き絶対値表現から 2 の補数への変換」と「2 の補数から符号付き絶対値表現への変換」は同じ操作(先にビット反転してから 1 を加える)で実現できます。

    もちろん、逆演算を用いて 2 の補数 \\(1110\\) の符号付き絶対値表現を求めることもでき、その場合は「先に 1 を引いてからビット反転する」ことになります。

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

    まとめると、「先にビット反転してから 1 を加える」と「先に 1 を引いてからビット反転する」の 2 つの演算は、どちらも \\(10000\\) への補数を計算しており、等価です。

    本質的には、「ビット反転」という操作は実際には \\(1111\\) への補数を求めています(常に 符号付き絶対値表現 + 1 の補数 = 1111 が成り立つため)。そして、1 の補数にさらに 1 を加えて得られる 2 の補数が、\\(10000\\) への補数です。

    上記では \\(n = 4\\) を例にしましたが、この考え方は任意のビット長の 2 進数に一般化できます。

    ","path":["第 3 章   データ構造","3.5   まとめ"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"第 12 章   分割統治","text":"

    Abstract

    難題は段階的に分解され、そのたびにより単純になっていく。

    分割統治は一つの重要な事実を示している。単純なことから始めれば、すべてはもはや複雑ではない。

    ","path":["第 12 章   分割統治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"章の内容","text":"
    • 12.1   分割統治法
    • 12.2   分割統治探索戦略
    • 12.3   二分木の構築問題
    • 12.4   ハノイの塔の問題
    • 12.5   まとめ
    ","path":["第 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

    長さ \\(n\\) の昇順配列 nums が与えられ、そのすべての要素は一意である。要素 target を探索せよ。

    分割統治の観点から、探索区間 \\([i, j]\\) に対応する部分問題を \\(f(i, j)\\) と記す。

    元の問題 \\(f(0, n-1)\\) を出発点として、次の手順で二分探索を行う。

    1. 探索区間 \\([i, j]\\) の中点 \\(m\\) を計算し、それに基づいて探索区間の半分を除外する。
    2. 規模が半分に縮小された部分問題を再帰的に解く。候補は \\(f(i, m-1)\\) または \\(f(m+1, j)\\) である。
    3. 1.2. の手順を繰り返し、target が見つかるか区間が空になったら返す。

    次の図は、配列内で要素 \\(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   二分木を構築する例のデータ

    ","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1","level":3,"title":"1.   分割統治問題かどうかを判断する","text":"

    元の問題は preorderinorder から二分木を構築することであり、典型的な分割統治問題です。

    • 問題は分解できる:分割統治の観点から見ると、元の問題は 2 つの部分問題、すなわち左部分木の構築と右部分木の構築に分けられ、さらに根ノードを初期化する 1 ステップが加わります。各部分木(部分問題)に対しても、同じ分割方法を再利用してより小さな部分木(部分問題)へと分けていき、最小の部分問題(空部分木)に達した時点で終了します。
    • 部分問題は独立している:左部分木と右部分木は互いに独立しており、両者の間に重なりはありません。左部分木を構築するときは、中順走査と前順走査のうち左部分木に対応する部分だけを見れば十分です。右部分木も同様です。
    • 部分問題の解は統合できる:左部分木と右部分木(部分問題の解)が得られたら、それらを根ノードに接続することで元の問題の解を得られます。
    ","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   部分木をどのように分割するか","text":"

    以上の分析より、この問題は分割統治で解けます。では、前順走査 preorder と中順走査 inorder を使って左部分木と右部分木をどのように分割すればよいのでしょうか?

    定義に従うと、preorderinorder はいずれも 3 つの部分に分けられます。

    • 前順走査:[ 根ノード | 左部分木 | 右部分木 ] ,例えば上図の木は [ 3 | 9 | 2 1 7 ] に対応します。
    • 中順走査:[ 左部分木 | 根ノード | 右部分木 ] ,例えば上図の木は [ 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":"

    以上の分割方法により、**根ノード、左部分木、右部分木が preorderinorder の中で占めるインデックス区間**が得られました。これらのインデックス区間を表すために、いくつかのポインタ変数を導入します。

    • 現在の木の根ノードが preorder に現れるインデックスを \\(i\\) とします。
    • 現在の木の根ノードが inorder に現れるインデックスを \\(m\\) とします。
    • 現在の木が inorder において占めるインデックス区間を \\([l, r]\\) とします。

    次の表のように、これらの変数を用いれば根ノードの 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
    コードの可視化

    全画面で見る >

    下図は二分木を構築する再帰過程を示しています。各ノードは下向きに「再帰していく」過程で生成され、各辺(参照)は上向きに「戻る」過程で張られます。

    <1><2><3><4><5><6><7><8><9>

    図 12-8   二分木を構築する再帰過程

    各再帰関数における前順走査 preorder と中順走査 inorder の分割結果を下図に示します。

    図 12-9   各再帰関数での分割結果

    木のノード数を \\(n\\) とすると、各ノードの初期化(再帰関数 dfs() の 1 回の実行)には \\(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)は、問題を分けて統べるという意味であり、非常に重要で一般的なアルゴリズム戦略です。分割統治法は通常、再帰に基づいて実装され、「分」と「治」の 2 つのステップから構成されます。

    1. 分(分割段階):元の問題を 2 つ以上の部分問題へ再帰的に分解し、最小の部分問題に到達した時点で停止します。
    2. 治(統合段階):解が既知である最小の部分問題から始めて、部分問題の解を下から上へ統合し、元の問題の解を構築します。

    以下の図に示すように、「マージソート」は分割統治戦略の典型的な応用の一つです。

    1. 分:元の配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割し、部分配列に要素が 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. 部分問題の解は統合できる:元の問題の解は、部分問題の解を統合することで得られます。

    明らかに、マージソートは以上の 3 つの判断基準を満たしています。

    1. 問題は分解できる:配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割します。
    2. 部分問題は独立している:各部分配列は独立にソートできます(部分問題は独立に解けます)。
    3. 部分問題の解は統合できる:2 つの整列済み部分配列(部分問題の解)は、1 つの整列済み配列(元の問題の解)に統合できます。
    ","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212","level":2,"title":"12.1.2   分割統治法で効率を高める","text":"

    分割統治法はアルゴリズムの問題を効果的に解けるだけでなく、多くの場合アルゴリズムの効率も高められます。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートが選択ソート、バブルソート、挿入ソートより高速ですが、これは分割統治戦略を適用しているためです。

    ここで次の疑問が生じます。なぜ分割統治法はアルゴリズム効率を高められるのでしょうか。その根本的な仕組みは何でしょうか?言い換えると、大きな問題を複数の部分問題に分解し、部分問題を解き、それらの解を統合して元の問題の解にするという手順は、なぜ元の問題を直接解くより効率的なのでしょうか。この問題は、操作回数と並列計算の 2 つの観点から議論できます。

    ","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1","level":3,"title":"1.   操作回数の最適化","text":"

    「バブルソート」を例に取ると、長さ \\(n\\) の配列を処理するのに \\(O(n^2)\\) の時間がかかります。以下の図のように、配列を中央で 2 つの部分配列に分けると仮定すると、分割には \\(O(n)\\) の時間、各部分配列のソートには \\(O((n / 2)^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\\) のときに分割後の操作回数の方が少なくなり、ソート効率が高くなることを意味します。ただし、分割後の時間計算量は依然として 2 次の \\(O(n^2)\\) であり、計算量の定数項が小さくなっただけです。

    さらに考えると、部分配列を中央からさらに 2 つの部分配列へと分割し続け、部分配列に要素が 1 つだけ残るまで分割を止めないとしたらどうでしょうか。この考え方がまさに「マージソート」であり、時間計算量は \\(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":"

    一方では、分割統治法は多くの古典的なアルゴリズム問題を解くのに使えます。

    • 最近点対探索:このアルゴリズムは、まず点集合を 2 つに分け、それぞれの部分における最近点対を求め、最後に 2 つの部分をまたぐ最近点対を求めます。
    • 大整数乗算:たとえば Karatsuba 法では、大整数の乗算を、より小さな整数どうしのいくつかの乗算と加算に分解します。
    • 行列乗算:たとえば Strassen 法では、大きな行列の乗算を、複数の小さな行列の乗算と加算に分解します。
    • ハノイの塔問題:ハノイの塔問題は再帰によって解くことができ、これは典型的な分割統治戦略の応用です。
    • 反転対の計算:ある数列で前の数が後ろの数より大きい場合、その 2 つの数は反転対を構成します。反転対の問題は、分割統治の考え方を利用し、マージソートを用いて解けます。

    他方で、分割統治法はアルゴリズムとデータ構造の設計にも非常に広く応用されています。

    • 二分探索:二分探索では、整列済み配列を中央のインデックスで 2 つに分け、目標値と中央要素の比較結果に基づいてどちらの半区間を除外するかを決め、残った区間で同じ二分操作を行います。
    • マージソート:本節の冒頭で紹介したため、ここでは繰り返しません。
    • クイックソート:クイックソートは基準値を 1 つ選び、配列を、基準値より小さい要素の部分配列と、基準値より大きい要素の部分配列に分け、その後それぞれに対して同じ分割操作を行い、部分配列に要素が 1 つだけ残るまで続けます。
    • バケットソート:バケットソートの基本的な考え方は、データを複数のバケットに分散し、各バケット内の要素をソートしたうえで、各バケットの要素を順に取り出して整列済み配列を得ることです。
    • 木構造:たとえば二分探索木、AVL 木、赤黒木、B 木、B+ 木などでは、探索・挿入・削除などの操作をいずれも分割統治戦略の応用とみなせます。
    • ヒープ:ヒープは特殊な完全二分木であり、挿入、削除、ヒープ化などの各種操作には、実際には分割統治の考え方が含まれています。
    • ハッシュテーブル:ハッシュテーブル自体は分割統治を直接適用しているわけではありませんが、いくつかのハッシュ衝突解決法では間接的に分割統治戦略が使われています。たとえば、連鎖アドレス法における長い連結リストは、検索効率を高めるために赤黒木へ変換されます。

    このように、**分割統治法は「静かに物を潤す」ようなアルゴリズム思想**であり、さまざまなアルゴリズムやデータ構造の中に潜んでいます。

    ","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   ハノイの塔の問題","text":"

    マージソートや二分木の構築では、いずれも元の問題を元問題の半分の規模をもつ 2 つの部分問題に分解していました。しかし、ハノイの塔の問題では、異なる分解戦略を採用します。

    Question

    3 本の柱があり、それぞれを ABC とします。初期状態では、柱 A に \\(n\\) 枚の円盤が通されており、上から下へ小さい順に並んでいます。私たちの課題は、この \\(n\\) 枚の円盤を柱 C に移し、元の順序を保つことです(以下の図のとおり)。円盤を移動する際には、次のルールに従う必要があります。

    1. 円盤は 1 本の柱の頂上から取り出し、別の柱の頂上に置くことしかできません。
    2. 1 回に移動できる円盤は 1 枚だけです。
    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":"

    以下の図に示すように、問題 \\(f(1)\\) 、すなわち円盤が 1 枚だけの場合は、それを A から C へ直接移動すれば済みます。

    <1><2>

    図 12-11   規模 1 の問題の解

    以下の図に示すように、問題 \\(f(2)\\) 、すなわち円盤が 2 枚ある場合は、小さい円盤が常に大きい円盤の上にある条件を満たすため、B を借りて移動を行う必要があります。

    1. まず上の小さい円盤を A から B へ移します。
    2. 次に大きい円盤を A から C へ移します。
    3. 最後に小さい円盤を B から C へ移します。
    <1><2><3><4>

    図 12-12   規模 2 の問題の解

    問題 \\(f(2)\\) を解く過程は、**2 枚の円盤を B を介して A から C へ移す**と要約できます。このとき、C を目標の柱、B を補助の柱と呼びます。

    ","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2","level":3,"title":"2.   部分問題への分解","text":"

    問題 \\(f(3)\\) 、すなわち円盤が 3 枚ある場合になると、状況はやや複雑になります。

    \\(f(1)\\) と \\(f(2)\\) の解が既知なので、分割統治の観点から、A の上部にある 2 枚の円盤をひとまとまりとみなして、次の図の手順を実行できます。こうして 3 枚の円盤を A から C へ順調に移動できます。

    1. B を目標の柱、C を補助の柱として、2 枚の円盤を A から B へ移します。
    2. A に残った 1 枚の円盤を A から C へ直接移動します。
    3. C を目標の柱、A を補助の柱として、2 枚の円盤を B から C へ移します。
    <1><2><3><4>

    図 12-13   規模 3 の問題の解

    本質的には、問題 \\(f(3)\\) を 2 つの部分問題 \\(f(2)\\) と 1 つの部分問題 \\(f(1)\\) に分けています 。この 3 つの部分問題を順に解けば、元の問題も解決されます。これは、部分問題が独立しており、解を組み合わせられることを示しています。

    ここまでで、次の図に示すハノイの塔の問題を解く分割統治戦略をまとめられます。元の問題 \\(f(n)\\) を 2 つの部分問題 \\(f(n-1)\\) と 1 つの部分問題 \\(f(1)\\) に分け、次の順序でこの 3 つの部分問題を解きます。

    1. \\(n-1\\) 枚の円盤を C を介して A から B へ移します。
    2. 残り \\(1\\) 枚の円盤を A から C へ直接移します。
    3. \\(n-1\\) 枚の円盤を A を介して B から C へ移します。

    この 2 つの部分問題 \\(f(n-1)\\) は、同じ方法で再帰的に分割できます。最小の部分問題 \\(f(1)\\) に到達するまでこれを続けます。一方、\\(f(1)\\) の解は既知であり、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) を定義します。その役割は、柱 src の上部にある \\(i\\) 枚の円盤を、補助の柱 buf を使って目標の柱 tar へ移動することです:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
    def move(src: list[int], tar: list[int]):\n    \"\"\"円盤を 1 枚移動\"\"\"\n    # src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1:\n        move(src, tar)\n        return\n    # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf)\n    # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    # A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C)\n
    hanota.cpp
    /* 円盤を 1 枚移動 */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.java
    /* 円盤を 1 枚移動 */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.cs
    /* 円盤を 1 枚移動 */\nvoid Move(List<int> src, List<int> tar) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    DFS(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    Move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    DFS(n, A, B, C);\n}\n
    hanota.go
    /* 円盤を 1 枚移動 */\nfunc move(src, tar *list.List) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfsHanota(i-1, src, tar, buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* ハノイの塔を解く */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfsHanota(n, A, B, C)\n}\n
    hanota.swift
    /* 円盤を 1 枚移動 */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src: &src, tar: &tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // src の上から n 個の円盤を、B を介して C に移動する\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
    hanota.js
    /* 円盤を 1 枚移動 */\nfunction move(src, tar) {\n    // src の上から円盤を1枚取り出す\n    const pan = src.pop();\n    // 円盤を tar の上に置く\n    tar.push(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunction dfs(i, src, buf, tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.ts
    /* 円盤を 1 枚移動 */\nfunction move(src: number[], tar: number[]): void {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.dart
    /* 円盤を 1 枚移動 */\nvoid move(List<int> src, List<int> tar) {\n  // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n  dfs(i - 1, src, tar, buf);\n  // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n  move(src, tar);\n  // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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  // A の上から n 枚の円盤を B を介して C へ移す\n  dfs(n, A, B, C);\n}\n
    hanota.rs
    /* 円盤を 1 枚移動 */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move_pan(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
    hanota.c
    /* 円盤を 1 枚移動 */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, srcSize, tar, tarSize);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
    hanota.kt
    /* 円盤を 1 枚移動 */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // src の上から円盤を1枚取り出す\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 に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\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    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C)\n}\n
    hanota.rb
    ### 円盤を1枚移動 ###\ndef move(src, tar)\n  # src の上から円盤を1枚取り出す\n  pan = src.pop\n  # 円盤を tar の上に置く\n  tar << pan\nend\n\n### ハノイの塔 f(i) を解く ###\ndef dfs(i, src, buf, tar)\n  # src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n  dfs(i - 1, src, tar, buf)\n  # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n  move(src, tar)\n  # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n  dfs(i - 1, buf, src, tar)\nend\n\n### ハノイの塔を解く ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # A の上から n 枚の円盤を B を介して C へ移す\n  dfs(n, _A, _B, _C)\nend\n
    コードの可視化

    全画面で見る >

    以下の図に示すように、ハノイの塔の問題は高さ \\(n\\) の再帰木を形成し、各ノードは 1 つの部分問題、すなわち 1 つ起動された dfs() 関数に対応します。したがって時間計算量は \\(O(2^n)\\) 、空間計算量は \\(O(n)\\) です。

    図 12-15   ハノイの塔の問題の再帰木

    Quote

    ハノイの塔の問題は古い伝説に由来します。古代インドのある寺院で、僧侶たちは 3 本の高いダイヤモンドの柱と、\\(64\\) 枚の大きさの異なる金の円盤を持っていました。僧侶たちは絶えず円盤を動かし、最後の 1 枚が正しく置かれた瞬間に世界が終わると信じていました。

    しかし、たとえ僧侶たちが 1 秒に 1 回移動するとしても、合計でおよそ \\(2^{64} \\approx 1.84×10^{19}\\) 秒、約 \\(5850\\) 億年が必要で、現在推定されている宇宙の年齢をはるかに上回ります。したがって、この伝説が本当だったとしても、世界の終わりを心配する必要はなさそうです。

    ","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":"
    • 分割統治法は一般的なアルゴリズム設計戦略であり、分(分割)と治(統合)の 2 つの段階からなり、通常は再帰に基づいて実装されます。
    • それが分割統治法の問題かどうかを判断する基準には、問題を分解できるか、部分問題が独立しているか、部分問題を統合できるかが含まれます。
    • マージソートは分割統治法の典型的な応用であり、配列を再帰的に同じ長さの 2 つの部分配列に分割し、要素が 1 つだけになるまで続け、その後で各層を順に統合してソートを完了します。
    • 分割統治法を導入すると、多くの場合アルゴリズムの効率を高められます。一方では操作回数が減り、他方では分割後にシステムの並列最適化を行いやすくなります。
    • 分割統治法は多くのアルゴリズム問題を解決できるだけでなく、データ構造やアルゴリズム設計にも広く応用されており、至る所でその姿を見ることができます。
    • 総当たり探索と比べて、適応的な探索のほうが効率的です。時間計算量が \\(O(\\log n)\\) の探索アルゴリズムは、通常は分割統治法に基づいて実装されます。
    • 二分探索は分割統治法のもう 1 つの典型的な応用であり、部分問題の解を統合する手順を含みません。再帰的な分割統治によって二分探索を実現できます。
    • 二分木を構築する問題では、木の構築(元の問題)を左部分木と右部分木の構築(部分問題)に分けられます。これは、先行順序走査と中間順序走査のインデックス区間を分割することで実現できます。
    • ハノイの塔の問題では、規模 \\(n\\) の問題を、規模 \\(n-1\\) の 2 つの部分問題と規模 \\(1\\) の 1 つの部分問題に分けられます。これら 3 つの部分問題を順に解くと、元の問題も解決されます。
    ","path":["第 12 章   分割統治","12.5   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"第 14 章   動的計画法","text":"

    Abstract

    小川は川へと注ぎ、河川は大海へと注ぐ。

    動的計画法は小さな問題の解を集めて大きな問題の答えとし、一歩ずつ私たちを問題解決の彼岸へと導く。

    ","path":["第 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 章   動的計画法"],"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]\\) は地面(開始地点)です。頂上に到達するために必要な最小コストを求めてください。

    下図に示すように、第 \\(1\\)、\\(2\\)、\\(3\\) 段のコストがそれぞれ \\(1\\)、\\(10\\)、\\(1\\) である場合、地面から第 \\(3\\) 段まで上る最小コストは \\(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   階段昇り最小コストの動的計画法の過程

    この問題では空間最適化も可能であり、一次元をゼロ次元まで圧縮することで、空間計算量を \\(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\\) 段上ることができます。ただし、連続する 2 回で \\(1\\) 段ずつ上ることはできません。頂上まで上る方法は何通りあるでしょうか。

    下図に示すように、第 \\(3\\) 段まで上る実行可能な方法は \\(2\\) 通りしか残りません。そのうち、\\(1\\) 段ずつ 3 回連続で上る方法は制約を満たさないため除外されます。

    図 14-8   制約付きで第 3 段まで上る方法数

    この問題では、前回が \\(1\\) 段上りだった場合、次回は必ず \\(2\\) 段上らなければなりません。これは、**次の一手が現在の状態(現在いる階段の段数)だけでは独立に決まらず、一つ前の状態(前回いた段数)にも関係する**ことを意味します。

    容易に分かるように、この問題はもはや無後効性を満たしておらず、状態遷移方程式 \\(dp[i] = dp[i-1] + dp[i-2]\\) も成立しません。というのも、\\(dp[i-1]\\) は今回 \\(1\\) 段上る場合を表しますが、その中には「前回も \\(1\\) 段上ってきた」方法が多数含まれており、制約を満たすためには \\(dp[i-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]\\) から遷移できます。

    下図に示すように、この定義のもとでは \\(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 回でそれぞれ第 \\(2\\) 段、第 \\(3\\) 段に到達した場合、その後は第 \\(4\\) 段と第 \\(6\\) 段に跳ぶことはできません。頂上まで上る方法は何通りあるでしょうか。

    この問題では、次の跳躍が過去のすべての状態に依存します。なぜなら、各跳躍がより高い段に障害物を設置し、将来の跳躍に影響するからです。この種の問題は、動的計画法では解きにくいことが多いです。

    実際、多くの複雑な組合せ最適化問題(例えば巡回セールスマン問題)は無後効性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を用いて、限られた時間内に実用的な局所最適解を得ます。

    ","path":["第 14 章   動的計画法","14.2   動的計画法の問題特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   動的計画法の問題解決の考え方","text":"

    前の 2 節では動的計画法の問題の主要な特徴を紹介しました。ここからは、さらに実用的な 2 つの問題を一緒に考えていきます。

    1. ある問題が動的計画法の問題かどうかを、どのように判断すればよいでしょうか?
    2. 動的計画法の問題を解くには、どこから着手し、完全な手順はどのようなものでしょうか?
    ","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431","level":2,"title":"14.3.1   問題の判定","text":"

    一般に、ある問題が重複部分問題と最適部分構造を含み、さらに無後效性を満たしているなら、通常は動的計画法で解くのに適しています。しかし、問題文からこれらの性質を直接読み取るのは簡単ではありません。そのため通常は条件を少し緩めて、**まずその問題がバックトラッキング(全探索)で解くのに適しているか**を観察します。

    バックトラッキングで解くのに適した問題は、通常「決定木モデル」を満たします。この種の問題は木構造で表現でき、各ノードは 1 つの決定を表し、各経路は 1 つの決定列を表します。

    言い換えると、問題に明確な決定の概念が含まれており、解が一連の決定によって生成されるなら、その問題は決定木モデルを満たし、通常はバックトラッキングで解くことができます。

    これに加えて、動的計画法の問題には判定のための「加点要素」もあります。

    • 問題文に最大(最小)や最多(最少)などの最適化に関する記述がある。
    • 問題の状態が配列、多次元行列、または木で表現でき、ある状態とその周辺の状態の間に漸化的な関係がある。

    反対に、「減点要素」もあります。

    • 問題の目的が最適解を求めることではなく、あり得るすべての解を列挙することである。
    • 問題文に明確な順列・組合せの特徴があり、具体的な複数の解を返す必要がある。

    ある問題が決定木モデルを満たし、さらに比較的明確な「加点要素」を備えているなら、その問題は動的計画法の問題であると仮定し、解く過程でそれを検証できます。

    ","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432","level":2,"title":"14.3.2   問題を解く手順","text":"

    動的計画法の解法の流れは問題の性質や難易度によって異なりますが、通常は次の手順に従います。すなわち、決定を記述し、状態を定義し、\\(dp\\) テーブルを構築し、状態遷移方程式を導出し、境界条件を定めます。

    解法の手順をより具体的に示すために、ここでは古典的な問題である「最小経路和」を例にします。

    Question

    \\(n \\times m\\) の 2 次元グリッド grid が与えられます。グリッドの各セルには非負整数が格納されており、そのセルのコストを表します。ロボットは左上のセルを始点とし、毎回下または右に 1 マスだけ移動して、右下のセルまで進みます。左上から右下までの最小経路和を返してください。

    次の図は 1 つの例を示しており、このグリッドの最小経路和は \\(13\\) です。

    図 14-10   最小経路和のサンプルデータ

    ステップ 1:各ラウンドの決定を考え、状態を定義して、\\(dp\\) テーブルを得る

    この問題における各ラウンドの決定は、現在のマスから下または右へ 1 マス進むことです。現在のマスの行・列インデックスを \\([i, j]\\) とすると、下または右へ 1 マス進んだ後のインデックスは \\([i+1, j]\\) または \\([i, j+1]\\) になります。したがって、状態には行インデックスと列インデックスの 2 つの変数を含め、\\([i, j]\\) と表します。

    状態 \\([i, j]\\) に対応する部分問題は、始点 \\([0, 0]\\) から \\([i, j]\\) まで進む最小経路和であり、その解を \\(dp[i, j]\\) と記します。

    これで、次の図に示す 2 次元の \\(dp\\) 行列が得られます。そのサイズは入力グリッド \\(grid\\) と同じです。

    図 14-11   状態の定義と dp テーブル

    Note

    動的計画法とバックトラッキングの過程は、いずれも 1 つの決定列として記述できます。そして状態は、すべての決定変数から構成されます。状態には解法の進行状況を表すすべての変数が含まれているべきであり、次の状態を導くのに十分な情報を持っている必要があります。

    各状態は 1 つの部分問題に対応しており、すべての部分問題の解を保存するために \\(dp\\) テーブルを定義します。状態の各独立変数は、\\(dp\\) テーブルの 1 つの次元に対応します。本質的に、\\(dp\\) テーブルは状態と部分問題の解との対応関係です。

    ステップ 2:最適部分構造を見つけ、状態遷移方程式を導出する

    状態 \\([i, j]\\) は、上のマス \\([i-1, j]\\) または左のマス \\([i, j-1]\\) からしか遷移してきません。したがって最適部分構造は、\\([i, j]\\) に到達する最小経路和が、\\([i, j-1]\\) の最小経路和と \\([i-1, j]\\) の最小経路和のうち小さい方によって決まる、ということです。

    以上の分析から、次の図に示す状態遷移方程式を導くことができます。

    \\[ 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   境界条件と状態遷移の順序

    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]\\) 。
    • 戻り値:\\([0, 0]\\) から \\([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
    コードの可視化

    全画面で見る >

    次の図は、\\(dp[2, 1]\\) を根ノードとする再帰木を示しています。この中にはいくつかの重複部分問題が含まれており、その数はグリッド grid のサイズが大きくなるにつれて急激に増加します。

    本質的に、重複部分問題が生じる理由は、**左上からあるセルへ到達する経路が複数存在すること**にあります。

    図 14-14   力任せ探索の再帰木

    各状態には下と右の 2 通りの選択肢があり、左上から右下まで進むには合計で \\(m + n - 2\\) 歩必要です。したがって最悪時間計算量は \\(O(2^{m + n})\\) です。ここで、\\(n\\) と \\(m\\) はそれぞれグリッドの行数と列数を表します。なお、この見積もりではグリッド境界付近の状況を考慮していません。境界に達すると選択肢は 1 つだけになるため、実際の経路数はこれより少なくなります。

    ","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-2","level":3,"title":"2.   方法 2:メモ化探索","text":"

    グリッド grid と同じサイズのメモ配列 mem を導入し、各部分問題の解を記録して、重複部分問題を枝刈りします。

    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
    コードの可視化

    全画面で見る >

    次の図に示すように、メモ化を導入すると、すべての部分問題の解は 1 回だけ計算すればよくなります。したがって時間計算量は状態総数、すなわちグリッドサイズの \\(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
    コードの可視化

    全画面で見る >

    次の図は最小経路和の状態遷移の過程を示しています。グリッド全体を走査するため、時間計算量は \\(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":"

    各マスは左のマスと上のマスにのみ関係するため、1 行の配列だけを使って \\(dp\\) テーブルを実装できます。

    ただし、配列 dp は 1 行分の状態しか表せないため、先頭列の状態を事前に初期化することはできず、各行を走査するときに更新する必要があります。

    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":"

    編集距離は、Levenshtein 距離とも呼ばれ、2つの文字列の相互変換に必要な最小の編集回数を指し、通常は情報検索や自然言語処理において2つの系列の類似度を測るために用いられます。

    Question

    2つの文字列 \\(s\\) と \\(t\\) を入力し、\\(s\\) を \\(t\\) に変換するのに必要な最小編集回数を返してください。

    1つの文字列に対して3種類の編集操作を行えます。1文字の挿入、1文字の削除、任意の文字への置換です。

    下図に示すように、kittensitting に変換するには 3 回の編集が必要で、内訳は 2 回の置換と 1 回の挿入です。helloalgo に変換する場合も 3 回必要で、内訳は 2 回の置換と 1 回の削除です。

    図 14-27   編集距離のサンプルデータ

    編集距離問題は決定木モデルで自然に説明できます。文字列が木のノードに対応し、1回の決定(1回の編集操作)が木の1本の辺に対応します。

    下図に示すように、操作に制限がない場合、各ノードからは多くの辺を派生でき、それぞれの辺が1種類の操作に対応します。これは 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\\) に対して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\\) に対して1回の編集(挿入、削除、置換)を行い、両文字列の末尾の文字を同じにします。そうすることでそれらをスキップし、より小さい問題を考えられます。

    つまり、文字列 \\(s\\) に対する各ラウンドの決定(編集操作)は、\\(s\\) と \\(t\\) における残りの未一致文字を変化させます。したがって、状態は現在 \\(s\\) と \\(t\\) で考えている第 \\(i\\) と第 \\(j\\) 文字とし、\\([i, j]\\) と記します。

    状態 \\([i, j]\\) に対応する部分問題は、**\\(s\\) の先頭 \\(i\\) 文字を \\(t\\) の先頭 \\(j\\) 文字に変換するのに必要な最小編集回数**です。

    これにより、サイズが \\((i+1) \\times (j+1)\\) の2次元 \\(dp\\) テーブルが得られます。

    第2ステップ:最適部分構造を見つけ、状態遷移方程式を導く

    部分問題 \\(dp[i, j]\\) を考えます。これに対応する2つの文字列の末尾文字は \\(s[i-1]\\) と \\(t[j-1]\\) であり、編集操作の違いに応じて下図の3つの場合に分けられます。

    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]\\) の3つのうち最小の編集回数に、今回の編集回数 \\(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ステップ:境界条件と状態遷移の順序を決める

    2つの文字列がともに空のとき、編集回数は \\(0\\)、すなわち \\(dp[0, 0] = 0\\) です。\\(s\\) が空で \\(t\\) が空でないとき、最小編集回数は \\(t\\) の長さに等しいため、先頭行は \\(dp[0, j] = j\\) です。\\(s\\) が空でなく \\(t\\) が空のとき、最小編集回数は \\(s\\) の長さに等しいため、先頭列は \\(dp[i, 0] = i\\) です。

    状態遷移方程式を観察すると、\\(dp[i, j]\\) の解は左、上、左上の解に依存します。そのため、2重ループで \\(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                # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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        // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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        # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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
    コードの可視化

    全画面で見る >

    下図に示すように、編集距離問題の状態遷移の過程はナップサック問題と非常によく似ており、どちらも2次元グリッドを埋めていく過程とみなせます。

    <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-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                # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            else:\n                # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftUp\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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        // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[j] = leftup;\n      } else {\n        // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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        # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[j] = leftup\n      else\n        # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 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\\) 段上ることができます。頂上まで到達する方法は何通りあるでしょうか?

    次の図に示すように、\\(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 -1\\) 段目または \\(i - 2\\) 段目からしか \\(i\\) 段目へ進めません。

    ここから重要な帰結が得られます。**\\(i - 1\\) 段目まで上る方法数と \\(i - 2\\) 段目まで上る方法数の和が、\\(i\\) 段目まで上る方法数に等しい**のです。式は次のとおりです:

    \\[ dp[i] = dp[i-1] + dp[i-2] \\]

    これは、階段を上る問題では各部分問題の間に漸化関係があり、**元の問題の解は部分問題の解から構築できる**ことを意味します。次の図はこの漸化関係を示しています。

    図 14-2   方法数の漸化関係

    漸化式に基づいて総当たり探索の解法を得ることができます。\\(dp[n]\\) を出発点とし、**より大きな問題を再帰的に 2 つのより小さな問題の和へ分解**していき、最小部分問題 \\(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
    コードの可視化

    全画面で見る >

    次の図は総当たり探索によって形成される再帰木を示しています。問題 \\(dp[n]\\) に対して、その再帰木の深さは \\(n\\)、時間計算量は \\(O(2^n)\\) です。指数オーダーは爆発的に増加するため、比較的大きな \\(n\\) を入力すると長時間待たされることになります。

    図 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":"

    アルゴリズム効率を高めるため、**すべての重複部分問題を 1 回だけ計算したい**と考えます。そのために、各部分問題の解を記録する配列 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
    コードの可視化

    全画面で見る >

    次の図を見ると、メモ化を行うことで、すべての重複部分問題は 1 回だけ計算すればよくなり、時間計算量は \\(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   階段上りの動的計画法の過程

    バックトラッキング法と同様に、動的計画法でも問題解決の特定段階を表すために「状態」という概念を用います。各状態は 1 つの部分問題と、それに対応する局所最適解に対応します。たとえば、階段を上る問題では、状態は現在いる階段の段数 \\(i\\) と定義されます。

    以上を踏まえると、動的計画法のよく使われる用語を次のようにまとめられます。

    • 配列 dp を dp テーブル と呼び、\\(dp[i]\\) は状態 \\(i\\) に対応する部分問題の解を表します。
    • 最小部分問題に対応する状態(第 \\(1\\) 段目と第 \\(2\\) 段目の階段)を初期状態と呼びます。
    • 漸化式 \\(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 を使う必要はありません。2 つの変数を順に更新していくだけで十分です。コードは次のとおりです:

    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":"

    ナップサック問題は、動的計画法の入門として非常に適した問題であり、動的計画法で最もよく見られる問題形式の1つです。これには 0-1 ナップサック問題、完全ナップサック問題、多重ナップサック問題など、多くの派生があります。

    本節では、まず最も一般的な 0-1 ナップサック問題を解いていきます。

    Question

    \\(n\\) 個の品物が与えられ、\\(i\\) 番目の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量 \\(cap\\) のナップサックがあります。各品物は1回しか選べないとき、ナップサック容量の制約下で入れられる品物の最大価値を求めてください。

    以下の図を見てみましょう。品物番号 \\(i\\) は \\(1\\) から始まり、配列のインデックスは \\(0\\) から始まるため、品物 \\(i\\) は重さ \\(wgt[i-1]\\)、価値 \\(val[i-1]\\) に対応します。

    図 14-17   0-1 ナップサックのサンプルデータ

    0-1 ナップサック問題は、\\(n\\) 回の意思決定からなる過程とみなせます。各品物について「入れない」「入れる」という2つの選択肢があるため、この問題は決定木モデルを満たします。

    この問題の目的は「ナップサック容量の制約下で入れられる品物の最大価値」を求めることなので、動的計画法の問題である可能性が高いです。

    ステップ1:各ラウンドの選択を考え、状態を定義して、\\(dp\\) テーブルを得る

    各品物について、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。ここから状態を、現在の品物番号 \\(i\\) とナップサック容量 \\(c\\) として定義し、\\([i, c]\\) と表せます。

    状態 \\([i, c]\\) に対応する部分問題は、先頭 \\(i\\) 個の品物を容量 \\(c\\) のナップサックに入れるときの最大価値 であり、これを \\(dp[i, c]\\) と記します。

    求めるべきものは \\(dp[n, cap]\\) なので、サイズ \\((n+1) \\times (cap+1)\\) の2次元 \\(dp\\) テーブルが必要です。

    ステップ2:最適部分構造を見つけ、状態遷移方程式を導く

    品物 \\(i\\) に対する選択を行った後に残るのは、先頭 \\(i-1\\) 個の品物に対する部分問題であり、次の2つのケースに分けられます。

    • 品物 \\(i\\) を入れない :ナップサック容量は変わらず、状態は \\([i-1, c]\\) に変化します。
    • 品物 \\(i\\) を入れる :ナップサック容量は \\(wgt[i-1]\\) だけ減少し、価値は \\(val[i-1]\\) だけ増加して、状態は \\([i-1, c-wgt[i-1]]\\) に変化します。

    以上の分析から、この問題の最適部分構造が分かります。すなわち、最大価値 \\(dp[i, c]\\) は、品物 \\(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]\\) はいずれも \\(0\\) になります。

    現在の状態 \\([i, c]\\) は、上側の状態 \\([i-1, c]\\) と左上の状態 \\([i-1, c-wgt[i-1]]\\) から遷移してくるため、2重ループで \\(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    # 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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  // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2つの案のうち価値が大きいほうを返す\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  # 2つの案のうち価値が大きいほうを返す\n  [no, yes].max\nend\n
    コードの可視化

    全画面で見る >

    以下の図のように、各品物ごとに「選ばない」「選ぶ」の2つの探索分岐が生じるため、時間計算量は \\(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    # 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2つの案のうち価値が大きいほうを返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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  // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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    // 2 つの案のうち価値が大きい方を記録して返す\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  # 2 つの案のうち価値が大きい方を記録して返す\n  mem[i][c] = [no, yes].max\nend\n
    コードの可視化

    全画面で見る >

    次の図は、メモ化探索で剪定された探索分岐を示しています。

    図 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
    コードの可視化

    全画面で見る >

    以下の図のように、時間計算量と空間計算量はいずれも配列 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":"

    各状態は直前の行の状態にしか依存しないため、2つの配列をローテーションして用いることで、空間計算量を \\(O(n^2)\\) から \\(O(n)\\) に削減できます。

    さらに考えると、1つの配列だけで空間最適化を実現できるでしょうか。観察すると、各状態は真上または左上のマスから遷移してきます。配列が1つしかないと仮定すると、\\(i\\) 行目の走査を開始した時点では、その配列にはまだ \\(i-1\\) 行目の状態が格納されています。

    • 順方向に走査すると、\\(dp[i, j]\\) に到達した時点で、左上にある \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) の値がすでに上書きされている可能性があり、正しい状態遷移結果を得られません。
    • 逆方向に走査すれば、上書きの問題は発生せず、状態遷移を正しく行えます。

    次の図は、単一配列のもとで \\(i = 1\\) 行目から \\(i = 2\\) 行目へ変換する過程を示しています。順方向走査と逆方向走査の違いを考えてみてください。

    <1><2><3><4><5><6>

    図 14-21   0-1 ナップサックの空間最適化後の動的計画法の過程

    コード実装では、配列 dp の第1次元 \\(i\\) をそのまま削除し、内側のループを逆方向走査に変更するだけで済みます。

    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":"
    • 動的計画法は問題を分解し、部分問題の解を保存することで重複計算を避け、計算効率を高めます。
    • 時間を考慮しなければ、すべての動的計画法の問題はバックトラッキング(総当たり探索)で解けますが、再帰木には大量の重複部分問題が存在するため、効率はきわめて低くなります。メモ化配列を導入すると、計算済みのすべての部分問題の解を保存でき、重複部分問題が 1 回だけ計算されることを保証できます。
    • メモ化探索はトップダウンの再帰的解法であり、それに対応する動的計画法はボトムアップの漸化式による解法で、ちょうど「表を埋める」ようなものです。現在の状態は一部の局所状態にのみ依存するため、\\(dp\\) 表の 1 次元を削減して空間計算量を下げることができます。
    • 部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治、動的計画法、バックトラッキングではそれぞれ異なる性質を持ちます。
    • 動的計画法の問題には 3 つの大きな特徴があります。重複部分問題、最適部分構造、無後効性です。
    • 元の問題の最適解が部分問題の最適解から構築できるなら、その問題は最適部分構造を持ちます。
    • 無後効性とは、ある状態の将来の発展がその状態のみに関係し、過去に経たすべての状態とは無関係であることを指します。多くの組合せ最適化問題は無後効性を持たず、動的計画法で高速に解くことはできません。

    ナップサック問題

    • ナップサック問題は最も典型的な動的計画法の問題の 1 つであり、0-1 ナップサック、完全ナップサック、多重ナップサックなどの派生があります。
    • 0-1 ナップサックの状態は、容量 \\(c\\) のナップサックに対して、前 \\(i\\) 個の品物で得られる最大価値として定義されます。ナップサックに入れない場合と入れる場合の 2 つの判断から最適部分構造を得て、状態遷移方程式を構築できます。空間最適化では、各状態が真上と左上の状態に依存するため、左上の状態が上書きされるのを避けるために配列を逆順に走査する必要があります。
    • 完全ナップサック問題では各品物の選択数に制限がないため、品物を入れる場合の状態遷移は 0-1 ナップサック問題とは異なります。状態は真上と真左の状態に依存するので、空間最適化では順方向に走査するべきです。
    • コイン両替問題は完全ナップサック問題の変種です。「最大」価値を求める問題から「最小」の硬貨枚数を求める問題へ変わるため、状態遷移方程式の \\(\\max()\\) は \\(\\min()\\) に置き換える必要があります。また、ナップサック容量を「超えない」ことを目指すのではなく、目標金額を「ちょうど」作ることを目指すため、\\(amt + 1\\) を「目標金額を作れない」無効解の表現として用います。
    • コイン両替問題 II では、「最少硬貨枚数」を求める問題から「硬貨の組合せ数」を求める問題へ変わるため、状態遷移方程式も \\(\\min()\\) から総和演算子へ対応して変わります。

    編集距離問題

    • 編集距離(Levenshtein 距離)は 2 つの文字列間の類似度を測るために用いられ、ある文字列を別の文字列へ変換するための最小編集回数として定義されます。編集操作には追加、削除、置換が含まれます。
    • 編集距離問題の状態は、\\(s\\) の前 \\(i\\) 文字を \\(t\\) の前 \\(j\\) 文字へ変更するのに必要な最小編集回数として定義されます。\\(s[i] \\ne t[j]\\) のときは、追加、削除、置換の 3 つの判断があり、それぞれに対応する残りの部分問題があります。これにより最適部分構造を見いだし、状態遷移方程式を構築できます。一方、\\(s[i] = t[j]\\) のときは現在の文字を編集する必要はありません。
    • 編集距離では、状態は真上、真左、左上の状態に依存します。そのため、空間最適化後は順方向でも逆方向でも正しく状態遷移できません。そこで、変数を 1 つ用いて左上の状態を一時保存し、完全ナップサック問題と等価な形へ変換することで、空間最適化後に順方向走査を行えるようにします。
    ","path":["第 14 章   動的計画法","14.7   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   完全ナップサック問題","text":"

    本節では、まずもう 1 つの代表的なナップサック問題である完全ナップサック問題を解き、その特殊例である硬貨交換問題について見ていきます。

    ","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   完全ナップサック問題のサンプルデータ

    ","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1","level":3,"title":"1.   動的計画法の考え方","text":"

    完全ナップサック問題は 0-1 ナップサック問題と非常によく似ています。違いは、品物の選択回数に制限がない点だけです。

    • 0-1 ナップサック問題では、各品物は 1 つしかないため、品物 \\(i\\) をナップサックに入れた後は先頭 \\(i-1\\) 個の品物からしか選べません。
    • 完全ナップサック問題では、各品物の数は無限であるため、品物 \\(i\\) をナップサックに入れた後も、引き続き先頭 \\(i\\) 個の品物から選べます。

    完全ナップサック問題では、状態 \\([i, c]\\) の変化は 2 つの場合に分けられます。

    • 品物 \\(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":"

    2 つの問題のコードを比較すると、状態遷移の中で 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 の第 1 次元を削除するだけです。

    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":"

    ナップサック問題は動的計画法の代表的な問題群であり、多くの派生問題があります。硬貨交換問題もその 1 つです。

    Question

    \\(n\\) 種類の硬貨が与えられ、\\(i\\) 番目の硬貨の額面は \\(coins[i - 1]\\) 、目標金額は \\(amt\\) です。各硬貨は繰り返し選択できます。目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は \\(-1\\) を返します。例を以下の図に示します。

    図 14-24   硬貨交換問題のサンプルデータ

    ","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_1","level":3,"title":"1.   動的計画法の考え方","text":"

    硬貨交換は完全ナップサック問題の特殊なケースとみなせます。両者には次の対応関係と相違点があります。

    • 2 つの問題は相互に変換でき、「品物」は「硬貨」、「品物の重さ」は「硬貨の額面」、「ナップサック容量」は「目標金額」に対応します。
    • 最適化の目標は逆であり、完全ナップサック問題は品物価値の最大化、硬貨交換問題は硬貨枚数の最小化を目指します。
    • 完全ナップサック問題はナップサック容量を「超えない」解を求めますが、硬貨交換は目標金額に「ちょうど」一致する解を求めます。

    ステップ 1:各ラウンドの選択を考え、状態を定義して、\\(dp\\) テーブルを得る

    状態 \\([i, a]\\) に対応する部分問題は、**先頭 \\(i\\) 種類の硬貨で金額 \\(a\\) を作るための最小硬貨枚数**であり、これを \\(dp[i, a]\\) と表します。

    2 次元 \\(dp\\) テーブルのサイズは \\((n+1) \\times (amt+1)\\) です。

    ステップ 2:最適部分構造を見つけ、状態遷移方程式を導く

    本問の状態遷移方程式は、完全ナップサック問題と比べて次の 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\\) になります。

    硬貨が 1 枚もない場合、任意の \\(> 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
    コードの可視化

    全画面で見る >

    次の図は硬貨交換の動的計画法の過程を示しており、完全ナップサック問題と非常によく似ています。

    <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   硬貨交換問題 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)\\) の 2 次元行列です。

    現在の状態における組合せ数は、現在の硬貨を選ばない場合と選ぶ場合の 2 つの選択肢の組合せ数の和に等しくなります。状態遷移方程式は次のとおりです。

    \\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

    目標金額が \\(0\\) のときは、どの硬貨も選ばなくても目標金額を作れるため、先頭列のすべての \\(dp[i, 0]\\) を \\(1\\) に初期化します。硬貨がないときは、任意の \\(>0\\) の目標金額を作れないため、先頭行のすべての \\(dp[0, a]\\) は \\(0\\) になります。

    ","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 章   グラフ"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"章の内容","text":"
    • 9.1   グラフ
    • 9.2   グラフの基本操作
    • 9.3   グラフの走査
    • 9.4   まとめ
    ","path":["第 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   連結リスト、木、グラフの関係

    ","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#911","level":2,"title":"9.1.1   グラフの一般的な種類と用語","text":"

    辺が方向性を持つかどうかに応じて、無向グラフ(undirected graph)と有向グラフ(directed graph)に分けられます。次の図のとおりです。

    • 無向グラフでは、辺は 2 つの頂点間の「双方向」の接続関係を表します。例えば WeChat や QQ における「友だち関係」です。
    • 有向グラフでは、辺は方向性を持ち、すなわち \\(A \\rightarrow B\\) と \\(A \\leftarrow B\\) の 2 方向の辺は互いに独立です。例えば Weibo や Douyin における「フォロー」と「フォロワー」の関係です。

    図 9-2   有向グラフと無向グラフ

    すべての頂点が連結しているかどうかに応じて、連結グラフ(connected graph)と非連結グラフ(disconnected graph)に分けられます。次の図のとおりです。

    • 連結グラフでは、ある頂点から出発すると、ほかの任意の頂点に到達できます。
    • 非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。

    図 9-3   連結グラフと非連結グラフ

    辺に「重み」の変数を追加すると、次の図に示すような重み付きグラフ(weighted graph)が得られます。例えば『Honor of Kings』のようなモバイルゲームでは、システムが共にプレイした時間に基づいてプレイヤー間の「親密度」を計算します。この親密度ネットワークは重み付きグラフで表せます。

    図 9-4   重み付きグラフと重みなしグラフ

    グラフというデータ構造には、次のような基本用語があります。

    • 隣接(adjacency):2 つの頂点の間に辺が存在するとき、この 2 つの頂点は「隣接している」といいます。上図では、頂点 1 に隣接する頂点は 2、3、5 です。
    • 経路(path):頂点 A から頂点 B までに通過する辺で構成された列を、A から B への「経路」と呼びます。上図では、辺の列 1-5-2-4 は頂点 1 から頂点 4 への 1 本の経路です。
    • 次数(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\\) の行列を用いてグラフを表します。各行(列)は 1 つの頂点を表し、行列要素は辺を表します。\\(1\\) または \\(0\\) を用いて、2 つの頂点の間に辺があるかどうかを示します。

    次の図のように、隣接行列を \\(M\\)、頂点リストを \\(V\\) とすると、行列要素 \\(M[i, j] = 1\\) は頂点 \\(V[i]\\) から頂点 \\(V[j]\\) への辺が存在することを表し、逆に \\(M[i, j] = 0\\) は 2 つの頂点の間に辺がないことを表します。

    図 9-5   グラフの隣接行列による表現

    隣接行列には次の特徴があります。

    • 単純グラフでは、頂点は自分自身とは接続できないため、このとき隣接行列の主対角線上の要素には意味がありません。
    • 無向グラフでは、2 方向の辺は等価であるため、このとき隣接行列は主対角線に関して対称です。
    • 隣接行列の要素を \\(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   グラフの隣接リストによる表現

    隣接リストは実際に存在する辺だけを格納し、辺の総数は通常 \\(n^2\\) よりはるかに小さいため、より省スペースです。しかし、隣接リストでは辺を見つけるために連結リストを走査する必要があるため、時間効率は隣接行列に及びません。

    上図を見ると、隣接リストの構造はハッシュテーブルにおける「連鎖アドレス法」と非常によく似ているため、同様の方法で効率を最適化できます。例えば、連結リストが長い場合は 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   現実世界でよく見られるグラフ

    頂点 辺 グラフ計算問題 ソーシャルネットワーク ユーザー 友だち関係 潜在的な友だちの推薦 地下鉄路線 駅 駅間の接続性 最短経路の推薦 太陽系 天体 天体間の万有引力作用 惑星軌道の計算","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   グラフの基本操作","text":"

    グラフの基本操作は、「辺」に対する操作と「頂点」に対する操作に分けられます。「隣接行列」と「隣接リスト」の 2 つの表現方法では、実装方法が異なります。

    ","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#921","level":2,"title":"9.2.1   隣接行列に基づく実装","text":"

    頂点数が \\(n\\) の無向グラフを与えると、各種操作の実装方法は次図のとおりです。

    • 辺の追加または削除:隣接行列で指定した辺を直接変更すればよく、\\(O(1)\\) 時間です。無向グラフであるため、2 方向の辺を同時に更新する必要があります。
    • 頂点の追加:隣接行列の末尾に 1 行 1 列を追加し、すべてを \\(0\\) で埋めればよく、\\(O(n)\\) 時間です。
    • 頂点の削除:隣接行列から 1 行 1 列を削除します。先頭行と先頭列を削除する場合が最悪で、\\((n-1)^2\\) 個の要素を「左上へ移動」させる必要があるため、\\(O(n^2)\\) 時間です。
    • 初期化:\\(n\\) 個の頂点を受け取り、長さ \\(n\\) の頂点リスト vertices を初期化するのに \\(O(n)\\) 時間、サイズ \\(n \\times n\\) の隣接行列 adjMat を初期化するのに \\(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        # 隣接行列に 1 行追加\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        adjMat.emplace_back(vector<int>(n, 0));\n        // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\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        // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // 隣接行列に 1 列追加\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    // 隣接行列に 1 行追加\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 隣接行列に 1 列追加\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    // 隣接行列に 1 行追加\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // 隣接行列に 1 列追加\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        // 隣接行列に 1 行追加\n        self.adj_mat.push(vec![0; n]);\n        // 隣接行列に 1 列追加\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 列目を 0 にする\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        // 隣接行列に 1 行追加\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // 隣接行列に 1 列追加\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    # 隣接行列に 1 行追加\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # 隣接行列に 1 列追加\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)\\) 時間です。無向グラフなので、2 方向の辺を同時に追加する必要があります。
    • 辺の削除:頂点に対応する連結リストから指定した辺を探して削除するため、\\(O(m)\\) 時間です。無向グラフでは、2 方向の辺を同時に削除する必要があります。
    • 頂点の追加:隣接リストに 1 つの連結リストを追加し、新しい頂点をその連結リストの先頭ノードとするため、\\(O(1)\\) 時間です。
    • 頂点の削除:隣接リスト全体を走査し、指定した頂点を含むすべての辺を削除する必要があるため、\\(O(n + m)\\) 時間です。
    • 初期化:隣接リストに \\(n\\) 個の頂点と \\(2m\\) 本の辺を作成するため、\\(O(n + m)\\) 時間です。
    <1><2><3><4><5>

    図 9-8   隣接リストの初期化、辺の追加と削除、頂点の追加と削除

    以下は隣接リストのコード実装です。上図と比べると、実際のコードには次の違いがあります。

    • 頂点の追加と削除を容易にし、コードを簡潔にするため、連結リストの代わりにリスト(動的配列)を使用しています。
    • ハッシュテーブルを用いて隣接リストを格納しており、key は頂点インスタンス、value はその頂点に隣接する頂点のリスト(連結リスト)です。

    また、隣接リストでは頂点を表すために Vertex クラスを使用しています。その理由は、もし隣接行列と同様にリストのインデックスで異なる頂点を区別すると、インデックス \\(i\\) の頂点を削除する場合、隣接リスト全体を走査して、\\(i\\) より大きいすべてのインデックスを \\(1\\) 減らす必要があり、効率が非常に低いためです。これに対して、各頂点が一意な 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   隣接行列と隣接リストの比較

    隣接行列 隣接リスト(連結リスト) 隣接リスト(ハッシュテーブル) 隣接判定 \\(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)\\)

    上表を見ると、隣接リスト(ハッシュテーブル)の時間効率と空間効率が最も優れているように見えます。しかし実際には、隣接行列のほうが辺の操作効率は高く、必要なのは 1 回の配列アクセスまたは代入だけです。総合的に見ると、隣接行列は「空間を時間と引き換えにする」原則を体現し、隣接リストは「時間を空間と引き換えにする」原則を体現しています。

    ","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   グラフの走査","text":"

    木は「一対多」の関係を表すのに対し、グラフはより高い自由度を持ち、任意の「多対多」の関係を表現できます。したがって、木はグラフの一種の特殊な場合とみなせます。明らかに、木の走査操作もグラフの走査操作の一種の特殊な場合です。

    グラフと木はいずれも、走査操作を実現するために探索アルゴリズムを用いる必要があります。グラフの走査方法も、幅優先走査と深さ優先走査の 2 種類に分けられます。

    ","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931","level":2,"title":"9.3.1   幅優先走査","text":"

    幅優先走査は、近いところから遠いところへ向かう走査方法であり、ある頂点から出発して、常に最も近い頂点を優先して訪問し、層ごとに外側へ広がっていきます。以下の図に示すように、左上の頂点から出発し、まずその頂点のすべての隣接頂点を走査し、次に次の頂点のすべての隣接頂点を走査し、これを繰り返して、すべての頂点を訪問するまで続けます。

    図 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

    ハッシュ集合は、value を持たず key だけを格納するハッシュテーブルとみなせます。これは \\(O(1)\\) の時間計算量で key の追加・削除・検索・更新を行えます。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   グラフの幅優先走査の手順

    幅優先走査の順序列は一意ですか?

    一意ではありません。幅優先走査は「近いところから遠いところへ」の順で走査することだけを要求し、同じ距離にある複数の頂点の走査順は任意に入れ替えて構いません。上図を例にすると、頂点 \\(1\\) と \\(3\\) の訪問順は交換でき、頂点 \\(2\\)、\\(4\\)、\\(6\\) の訪問順も任意に入れ替えられます。

    ","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2","level":3,"title":"2.   計算量の分析","text":"

    時間計算量:すべての頂点は 1 回ずつキューに入り、1 回ずつキューから出るため、\\(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   グラフの深さ優先走査

    ","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 操作は新しい参照を返すため、元の参照を新しい slice の参照で再代入する必要がある\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   グラフの深さ優先走査の手順

    深さ優先走査の順序列は一意ですか?

    幅優先走査と同様に、深さ優先走査の順序列も一意ではありません。ある頂点が与えられたとき、どの方向を先に探索してもよく、つまり隣接頂点の順序は任意に入れ替えられ、それでも深さ優先走査になります。

    木の走査を例にすると、「根 \\(\\rightarrow\\) 左 \\(\\rightarrow\\) 右」「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」「左 \\(\\rightarrow\\) 右 \\(\\rightarrow\\) 根」は、それぞれ先行順、中間順、後行順走査に対応します。これらは 3 種類の走査優先順位を示していますが、いずれも深さ優先走査に属します。

    ","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":"
    • グラフは頂点と辺から構成され、一組の頂点と一組の辺からなる集合として表せます。
    • 線形関係(連結リスト)や分治関係(木)と比べて、ネットワーク関係(グラフ)は自由度が高く、そのぶん複雑です。
    • 有向グラフの辺は方向性を持ち、連結グラフでは任意の頂点に到達でき、重み付きグラフの各辺は重み変数を含みます。
    • 隣接行列は行列を用いてグラフを表し、各行(列)が 1 つの頂点を表し、行列要素が辺を表します。\\(1\\) または \\(0\\) を用いて、2 つの頂点の間に辺があるかないかを示します。隣接行列は追加・削除・検索・更新の操作効率が高い一方で、より多くの空間を消費します。
    • 隣接リストは複数の連結リストを使ってグラフを表し、第 \\(i\\) 個の連結リストが頂点 \\(i\\) に対応し、その頂点に隣接するすべての頂点を格納します。隣接リストは隣接行列よりも省スペースですが、辺を探すために連結リストを走査する必要があるため、時間効率は低くなります。
    • 隣接リスト内の連結リストが長くなりすぎた場合は、赤黒木やハッシュテーブルに変換することで、検索効率を高められます。
    • アルゴリズムの考え方という観点では、隣接行列は「空間を時間と引き換えにする」ことを体現し、隣接リストは「時間を空間と引き換えにする」ことを体現します。
    • グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実のシステムをモデル化するために使えます。
    • 木はグラフの特殊な一例であり、木の走査もグラフ走査の特殊な一例です。
    • グラフの幅優先探索は、近いところから遠いところへ、層ごとに広がっていく探索方法であり、通常はキューを使って実装します。
    • グラフの深さ優先探索は、まず行けるところまで進み、進めなくなったらバックトラックする探索方法であり、通常は再帰に基づいて実装します。
    ","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q:経路の定義は頂点列ですか、それとも辺列ですか?

    Wikipedia では言語版ごとに定義が一致していません。英語版では「経路は辺の列」であり、中国語版では「経路は頂点の列」です。以下は英語版の原文です:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

    本書では、経路を頂点列ではなく辺列とみなします。これは、2 つの頂点の間に複数の辺が存在する可能性があり、その場合は各辺がそれぞれ 1 本の経路に対応するためです。

    Q:非連結グラフには到達できない頂点がありますか?

    非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。非連結グラフ全体を走査するには、グラフ内のすべての連結成分をたどれるように複数の始点を設定する必要があります。

    Q:隣接リストにおいて、「その頂点に接続されたすべての頂点」の順序に決まりはありますか?

    順序は任意でかまいません。ただし実際の応用では、頂点を追加した順序や頂点値の大小順など、特定の規則に従って並べ替える必要がある場合があります。そうすることで、「ある種の極値を持つ」頂点をすばやく見つけやすくなります。

    ","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"第 15 章   貪欲法","text":"

    Abstract

    ヒマワリは太陽に向かって回り、自らが最も大きく成長できる可能性を常に追い求める。

    貪欲戦略は、一回ごとの単純な選択を通じて、徐々に最適な答えへと導く。

    ","path":["第 15 章   貪欲法"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"章の内容","text":"
    • 15.1   貪欲法
    • 15.2   分数ナップサック問題
    • 15.3   最大容量問題
    • 15.4   最大積分割問題
    • 15.5   まとめ
    ","path":["第 15 章   貪欲法"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   分数ナップサック問題","text":"

    Question

    \\(n\\) 個の品物が与えられ、第 \\(i\\) 個の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量が \\(cap\\) のナップサックがある。各品物は 1 回だけ選択できるが、品物の一部を選ぶこともでき、価値は選択した重量の割合に応じて計算される。容量制限の下でナップサック内の品物の最大価値を求めよ。例を以下に示す。

    図 15-3   分数ナップサック問題の例データ

    分数ナップサック問題は 0-1 ナップサック問題と全体として非常によく似ており、状態には現在の品物 \\(i\\) と容量 \\(c\\) が含まれ、目標は容量制限下での最大価値を求めることである。

    異なる点は、本問では品物の一部だけを選べることである。以下に示すように、品物は任意に分割でき、対応する価値は重量の割合に応じて計算される。

    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    # 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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  // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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    // 重さと価値の 2 属性を持つ品物リストを作成\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  # 重さと価値の 2 属性を持つ品物リストを作成する\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(\\log n)\\)、空間計算量は通常 \\(O(\\log n)\\) または \\(O(n)\\) であり、具体的な値はプログラミング言語の実装に依存する。

    ソートを除けば、最悪の場合は品物リスト全体を走査する必要があるため、時間計算量は \\(O(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   分数ナップサック問題の幾何学的表現

    ","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   コイン両替の貪欲戦略

    実装コードは次のとおりです。

    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
    コードの可視化

    全画面で見る >

    思わずこう言いたくなるかもしれません。So clean!貪欲法はわずか十行ほどのコードでコイン両替問題を解いてしまいます。

    ","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)\\) より 1 桁小さいオーダーです。

    しかし、硬貨の額面の組み合わせによっては、貪欲法では最適解を見つけられません。下図に 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   貪欲法では最適解を見つけられない例

    つまり、コイン両替問題に対して、貪欲法は大域最適解を保証できず、非常に悪い解を見つけてしまうこともあります。この問題は動的計画法で解くほうが適しています。

    一般に、貪欲法が適用できる状況は次の 2 つに分けられます。

    1. 最適解を保証できる場合:この場合、貪欲法はしばしば最良の選択です。多くの場合、バックトラッキングや動的計画法より効率的だからです。
    2. 近似最適解を見つけられる場合:この場合も貪欲法は有効です。多くの複雑な問題では、大域最適解を求めること自体が非常に難しく、より高い効率で準最適解を得られるだけでも十分価値があります。
    ","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512","level":2,"title":"15.1.2   貪欲法の特性","text":"

    では、どのような問題が貪欲法に適しているのでしょうか。言い換えると、貪欲法はどのような場合に最適解を保証できるのでしょうか。

    動的計画法と比べると、貪欲法の適用条件はより厳しく、主に次の 2 つの性質に注目します。

    • 貪欲選択性:局所最適な選択が常に大域最適解につながる場合にのみ、貪欲法は最適解を保証できます。
    • 最適部分構造:元の問題の最適解が、部分問題の最適解を含むことです。

    最適部分構造については「動的計画法」の節ですでに紹介したので、ここでは繰り返しません。なお、問題によっては最適部分構造が明確でなくても、貪欲法で解ける場合があります。

    ここでは主に、貪欲選択性をどのように判定するかを考えます。説明だけを見ると単純そうですが、実際には多くの問題で、貪欲選択性を証明するのは容易ではありません。

    たとえばコイン両替問題では、反例を挙げて貪欲選択性が成り立たないことを示すのは簡単ですが、成り立つことを証明するのは難しいです。もし、**どのような条件を満たす硬貨の組み合わせなら貪欲法で解けるのか**と問われると、直感や例示に頼った曖昧な答えしか出せず、厳密な数学的証明を与えるのは困難です。

    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":"

    貪欲法による問題解決の流れは、おおむね次の 3 段階に分けられます。

    1. 問題分析:状態の定義、最適化目標、制約条件などを整理し、問題の性質を理解します。この段階はバックトラッキングや動的計画法でも共通して現れます。
    2. 貪欲戦略の決定:各ステップでどのように貪欲選択を行うかを定めます。この戦略により各ステップで問題規模を縮小し、最終的に問題全体を解決します。
    3. 正しさの証明:通常は、その問題が貪欲選択性と最適部分構造を持つことを示す必要があります。この段階では、帰納法や背理法などの数学的証明が必要になることがあります。

    貪欲戦略を定めることは問題解決の核心ですが、実際には簡単ではないことも多く、主な理由は次のとおりです。

    • 問題ごとに貪欲戦略の差が大きい。多くの問題では貪欲戦略は比較的わかりやすく、おおまかな考察や試行だけで見つけられます。しかし複雑な問題では、貪欲戦略が非常に見えにくいことがあり、その場合は解法経験やアルゴリズム力が大きく問われます。
    • 一見もっともらしい貪欲戦略もある。自信を持って貪欲戦略を設計し、コードを書いて提出しても、一部のテストケースを通過できないことがあります。これは、その貪欲戦略が「部分的にしか正しくない」ためであり、先ほどのコイン両替は典型例です。

    正しさを保証するためには、貪欲戦略に対して厳密な数学的証明を行うべきであり、通常は背理法や数学的帰納法が必要になります。

    しかし、正しさの証明もまた簡単とは限りません。手がかりがない場合には、テストケースを使ってコードをデバッグしながら、貪欲戦略を少しずつ修正して検証していくことがよくあります。

    ","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514","level":2,"title":"15.1.4   貪欲法の典型問題","text":"

    貪欲法は、貪欲選択性と最適部分構造を満たす最適化問題によく用いられます。以下に典型的な貪欲法の問題をいくつか挙げます。

    • 硬貨のお釣り問題:ある種の硬貨の組み合わせでは、貪欲法で常に最適解が得られます。
    • 区間スケジューリング問題:いくつかのタスクがあり、それぞれがある時間区間で実行されるとします。できるだけ多くのタスクを完了することが目標で、毎回終了時刻が最も早いタスクを選ぶなら、貪欲法で最適解を得られます。
    • 分数ナップサック問題:一群の品物と積載容量が与えられたとき、総重量が容量を超えず、かつ総価値が最大になるように品物を選ぶ問題です。毎回、価値対重量比(価値 / 重量)が最も高い品物を選ぶなら、ある条件下で貪欲法は最適解を得られます。
    • 株式売買問題:株価の履歴が与えられ、複数回の売買が可能ですが、すでに株を保有している場合は売却前に再度購入することはできません。目標は最大利益を得ることです。
    • ハフマン符号化:ハフマン符号化は、可逆データ圧縮に用いられる貪欲法です。ハフマン木を構築する際、毎回出現頻度が最も低い 2 つのノードを選んで併合すると、最終的に得られるハフマン木の重み付きパス長(符号長)は最小になります。
    • Dijkstra アルゴリズム:与えられた始点から他の各頂点への最短経路問題を解く貪欲法です。
    ","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   最大容量問題","text":"

    Question

    配列 \\(ht\\) が与えられ、各要素は垂直な仕切り板の高さを表します。配列内の任意の 2 枚の仕切り板と、その間の空間で容器を構成できます。

    容器の容量は高さと幅の積(面積)に等しく、高さは短い方の仕切り板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。

    配列から 2 枚の仕切り板を選び、構成される容器の容量が最大となるようにしてください。最大容量を返します。例を以下の図に示します。

    図 15-7   最大容量問題のサンプルデータ

    容器は任意の 2 枚の仕切り板で囲まれるため、本問の状態は 2 枚の仕切り板のインデックスで表され、\\([i, j]\\) と記します。

    問題の条件より、容量は高さと幅の積に等しく、高さは短い板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。容量を \\(cap[i, j]\\) とすると、計算式は次のようになります。

    \\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

    配列の長さを \\(n\\) とすると、2 枚の仕切り板の組合せ数(状態総数)は \\(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":"

    この問題にはさらに効率的な解法があります。以下の図のように、状態 \\([i, j]\\) を 1 つ選び、インデックスが \\(i < j\\) かつ高さが \\(ht[i] < ht[j]\\) を満たすとします。つまり、\\(i\\) が短い板、\\(j\\) が長い板です。

    図 15-8   初期状態

    以下の図のように、このとき長い板 \\(j\\) を短い板 \\(i\\) に近づけると、容量は必ず小さくなります。

    これは、長い板 \\(j\\) を動かした後は幅 \\(j-i\\) が必ず小さくなるためです。また、高さは短い板で決まるので、高さは変わらない( \\(i\\) が依然として短い板)か、小さくなる(移動後の \\(j\\) が短い板になる)ことしかありません。

    図 15-9   長い板を内側へ動かした後の状態

    逆に考えると、短い板 \\(i\\) を内側へ縮めた場合にのみ、容量が大きくなる可能性があります。幅は必ず小さくなりますが、**高さは大きくなる可能性がある**からです(移動後の短い板 \\(i\\) がより長くなる可能性があります)。たとえば次の図では、短い板を動かした後に面積が大きくなっています。

    図 15-10   短い板を内側へ動かした後の状態

    以上から、本問の貪欲戦略を導けます。2 本のポインタを初期化して容器の両端に置き、各ラウンドで短い板に対応するポインタを内側へ縮め、2 本のポインタが出会うまで続けます。

    以下の図は、貪欲戦略の実行過程を示しています。

    1. 初期状態では、ポインタ \\(i\\) と \\(j\\) は配列の両端にあります。
    2. 現在の状態の容量 \\(cap[i, j]\\) を計算し、最大容量を更新します。
    3. 板 \\(i\\) と板 \\(j\\) の高さを比較し、短い板を内側へ 1 マス移動します。
    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    # 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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  // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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    // 2 枚の板が出会うまで貪欲選択を繰り返す\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  # 2 枚の板が出会うまで貪欲選択を繰り返す\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\\) を内側へ 1 マス動かすと、次の図に示す状態が「スキップ」されます。これは、その後それらの状態の容量を検証できないことを意味します。

    \\[ 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\\) が与えられたとき、それを少なくとも 2 つの正整数の和に分割し、分割後のすべての整数の積の最大値を求めよ。下図に示す。

    図 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":"

    経験的に、2 つの整数の積はその和より大きくなることが多い。\\(n\\) から因子 \\(2\\) を 1 つ切り出すと、それらの積は \\(2(n-2)\\) となる。この積を \\(n\\) と比較すると、

    \\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

    下図のように、\\(n \\geq 4\\) のとき、\\(2\\) を 1 つ切り出すと積は大きくなる。これは、\\(4\\) 以上の整数はすべて分割すべきことを意味する。

    貪欲戦略一:分割方法に \\(\\geq 4\\) の因子が含まれるなら、それはさらに分割すべきである。最終的な分割方法に現れる因子は \\(1\\)、\\(2\\)、\\(3\\) の 3 種類だけである。

    図 15-14   分割により積が大きくなる

    次に、どの因子が最適かを考える。\\(1\\)、\\(2\\)、\\(3\\) の 3 つの因子のうち、明らかに \\(1\\) が最も悪い。なぜなら \\(1 \\times (n-1) < n\\) は常に成り立ち、\\(1\\) を切り出すとかえって積が小さくなるからである。

    下図のように、\\(n = 6\\) のとき、\\(3 \\times 3 > 2 \\times 2 \\times 2\\) が成り立つ。これは、\\(2\\) を切り出すより \\(3\\) を切り出すほうが有利であることを意味する。

    貪欲戦略二:分割方法の中に存在してよい \\(2\\) は高々 2 つである。なぜなら、3 つの \\(2\\) は常に 2 つの \\(3\\) に置き換えられ、より大きな積を得られるからである。

    図 15-15   最適な分割因子

    以上より、次の貪欲戦略が導かれる。

    1. 整数 \\(n\\) を入力し、余りが \\(0\\)、\\(1\\)、\\(2\\) になるまで、そこから因子 \\(3\\) を繰り返し切り出す。
    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":"

    下図のように、ループで整数を分割する必要はなく、切り捨て除算によって \\(3\\) の個数 \\(a\\) を、剰余演算によって余り \\(b\\) を得られる。このとき、

    \\[ n = 3 a + b \\]

    なお、\\(n \\leq 3\\) の境界ケースでは、必ず \\(1\\) を 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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を 3 の個数、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 を例に取ると、よく使われるべき乗計算関数は 3 種類ある。

    • 演算子 ** と関数 pow() の時間計算量はいずれも \\(O(\\log⁡ a)\\) である。
    • 関数 math.pow() は内部で C 言語ライブラリの pow() 関数を呼び出し、浮動小数点のべき乗を実行するため、時間計算量は \\(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\\) :最適な分割方法に \\(\\geq 4\\) の因子 \\(x\\) が存在すると仮定すると、それは必ずさらに \\(2(x-2)\\) に分割でき、より大きい(または等しい)積が得られる。これは仮定に矛盾する。
    2. 分割方法に \\(1\\) は含まれない :最適な分割方法に因子 \\(1\\) が 1 つ存在すると仮定すると、それは必ず別の因子に併合でき、より大きい積を得られる。これは仮定に矛盾する。
    3. 分割方法に含まれる \\(2\\) は高々 2 つ :最適な分割方法に 3 つの \\(2\\) が含まれると仮定すると、それは必ず 2 つの \\(3\\) に置き換えられ、積はより大きくなる。これは仮定に矛盾する。
    ","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":"
    • 貪欲法は通常、最適化問題を解くために用いられ、その原理は各意思決定段階で局所最適な決定を行い、全体最適解を得ることを目指すというものである。
    • 貪欲法は反復的に次々と貪欲な選択を行い、各ラウンドで問題をより小さな部分問題へと変換し、最終的に問題を解決する。
    • 貪欲法は実装が簡単であるだけでなく、問題を解く効率も高い。動的計画法と比べると、貪欲法の時間計算量は通常より低い。
    • 硬貨両替問題では、ある種の硬貨の組み合わせに対しては貪欲法で最適解を保証できるが、別の組み合わせではそうではなく、非常に悪い解を見つけてしまう可能性がある。
    • 貪欲法による解法に適した問題は、貪欲選択性と最適部分構造という 2 つの性質を備えている。貪欲選択性は、貪欲戦略の有効性を表している。
    • 一部の複雑な問題では、貪欲選択性を証明するのは容易ではない。相対的には、反例による否定のほうが簡単であり、硬貨両替問題がその一例である。
    • 貪欲法の問題を解く流れは主に 3 段階に分かれる。すなわち、問題分析、貪欲戦略の決定、正しさの証明である。このうち、貪欲戦略の決定が中核であり、正しさの証明はしばしば難所となる。
    • 分数ナップサック問題は 0-1 ナップサックを基に、品物の一部を選ぶことを許しているため、貪欲法で解くことができる。貪欲戦略の正しさは背理法で証明できる。
    • 最大容量問題は全探索で解くことができ、時間計算量は \\(O(n^2)\\) である。貪欲戦略を設計し、各ラウンドで短い板を内側へ動かすことで、時間計算量を \\(O(n)\\) に最適化できる。
    • 最大分割積問題では、2 つの貪欲戦略を順に導いた。すなわち、\\(\\geq 4\\) の整数はすべてさらに分割すべきであり、最適な分割因子は \\(3\\) である。コードにはべき乗演算が含まれており、時間計算量はその実装方法に依存し、通常は \\(O(1)\\) または \\(O(\\log n)\\) である。
    ","path":["第 15 章   貪欲法","15.5   まとめ"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"第 6 章   ハッシュテーブル","text":"

    Abstract

    コンピュータの世界では、ハッシュテーブルは聡明な図書館員のような存在です。

    彼は請求記号の計算方法を知っており、そのため目的の本を素早く見つけられます。

    ","path":["第 6 章   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"章の内容","text":"
    • 6.1   ハッシュテーブル
    • 6.2   ハッシュ衝突
    • 6.3   ハッシュアルゴリズム
    • 6.4   まとめ
    ","path":["第 6 章   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   ハッシュアルゴリズム","text":"

    前の 2 節では、ハッシュテーブルの動作原理とハッシュ衝突の処理方法を紹介しました。しかし、オープンアドレス法であれ連鎖方式であれ、それらが保証できるのは衝突発生時でもハッシュテーブルが正常に動作することだけであり、ハッシュ衝突そのものを減らすことはできません。

    ハッシュ衝突があまりにも頻繁に発生すると、ハッシュテーブルの性能は急激に劣化します。下図のように、連鎖方式のハッシュテーブルでは、理想的な場合にはキーと値のペアが各バケットに均等に分布し、最良の検索効率を達成します。最悪の場合には、すべてのキーと値のペアが同じバケットに格納され、時間計算量は \\(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":"

    「高速かつ安定した」ハッシュテーブルというデータ構造を実現するために、ハッシュアルゴリズムは次の特徴を備える必要があります。

    • 決定性:同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成しなければなりません。そうして初めて、ハッシュテーブルの信頼性が保たれます。
    • 高効率:ハッシュ値の計算過程は十分に高速であるべきです。計算コストが小さいほど、ハッシュテーブルの実用性は高くなります。
    • 均一分布:ハッシュアルゴリズムは、キーと値のペアがハッシュテーブル内に均等に分布するようにすべきです。分布が均一であるほど、ハッシュ衝突の確率は低くなります。

    実際には、ハッシュアルゴリズムはハッシュテーブルの実装だけでなく、ほかの多くの分野でも広く利用されています。

    • パスワード保存:ユーザーのパスワードを保護するために、システムは通常、平文パスワードを直接保存せず、そのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力内容のハッシュ値を計算し、保存済みのハッシュ値と比較します。一致すれば、そのパスワードは正しいと見なされます。
    • データ完全性検査:送信側はデータのハッシュ値を計算してデータと一緒に送信できます。受信側は受け取ったデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。両者が一致すれば、そのデータは完全だと見なされます。

    暗号分野の応用では、ハッシュ値から元のパスワードを推測するといった逆解析を防ぐために、ハッシュアルゴリズムにはさらに高いレベルの安全性が求められます。

    • 一方向性:ハッシュ値から入力データに関するいかなる情報も逆算できないこと。
    • 耐衝突性:異なる 2 つの入力で同じハッシュ値になるものを見つけることが、極めて困難であること。
    • アバランシェ効果:入力のわずかな変化が、出力の大きく予測不能な変化を引き起こすこと。

    注意してほしいのは、**「均一分布」と「耐衝突性」は独立した 2 つの概念である**という点です。均一分布を満たしていても、耐衝突性を満たすとは限りません。たとえば、入力 key がランダムである場合、ハッシュ関数 key % 100 は均一に分布した出力を生成できます。しかし、このハッシュアルゴリズムはあまりにも単純で、下 2 桁が同じ key はすべて同じ出力になります。そのため、ハッシュ値から利用可能な key を容易に逆算でき、結果としてパスワードが破られてしまいます。

    ","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632","level":2,"title":"6.3.2   ハッシュアルゴリズムの設計","text":"

    ハッシュアルゴリズムの設計は、多くの要素を考慮しなければならない複雑な問題です。しかし、要求の高くない場面であれば、いくつかの単純なハッシュアルゴリズムを設計することもできます。

    • 加算ハッシュ:入力の各文字の ASCII コードを足し合わせ、その合計をハッシュ値とします。
    • 乗算ハッシュ:乗算の非相関性を利用し、各ラウンドで定数を掛けながら、各文字の ASCII コードをハッシュ値に累積します。
    • XOR ハッシュ:入力データの各要素を XOR 演算で 1 つのハッシュ値に累積します。
    • 回転ハッシュ:各文字の ASCII コードを 1 つのハッシュ値に累積し、各回の累積前にハッシュ値を回転させます。
    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\\) で割り切れるため、\\(3\\) で割り切れるすべての key は、\\(0\\)、\\(3\\)、\\(6\\) の 3 つのハッシュ値に写像されます。

    \\[ \\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\\) に置き換えると仮定すると、keymodulus の間に公約数が存在しないため、出力されるハッシュ値の均一性は明らかに向上します。

    \\[ \\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 などの標準的なハッシュアルゴリズムを用いることが一般的です。これらは任意長の入力データを、固定長のハッシュ値へ写像できます。

    ここ 1 世紀近くの間、ハッシュアルゴリズムは継続的に改良と最適化が進められてきました。ある研究者たちは性能向上に取り組み、別の研究者やハッカーたちは安全性の弱点を探し続けてきました。次の表は、実際の応用でよく使われるハッシュアルゴリズムを示したものです。

    • MD5 と SHA-1 は何度も攻撃に成功されているため、各種のセキュリティ用途では廃止されています。
    • SHA-2 系列の SHA-256 は最も安全なハッシュアルゴリズムの 1 つであり、いまだに成功した攻撃例がないため、多くのセキュリティ用途やプロトコルで広く使われています。
    • 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() 関数を呼び出して各種データ型のハッシュ値を計算できます。

    • 整数と真理値のハッシュ値は、その値自身です。
    • 浮動小数点数と文字列のハッシュ値の計算はやや複雑なので、興味がある読者は自分で調べてみてください。
    • タプルのハッシュ値は、各要素のハッシュ値を求めてから、それらを組み合わせて 1 つのハッシュ値にしたものです。
    • オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすれば、内容に基づくハッシュ値を実装できます。

    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 アルゴリズム\"\nhash_str = hash(str)\n# 文字列「Hello アルゴリズム」のハッシュ値は 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 アルゴリズム\";\nsize_t hashStr = hash<string>()(str);\n// 文字列「Hello アルゴリズム」のハッシュ値は 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 アルゴリズム\";\nint hashStr = str.hashCode();\n// 文字列「Hello アルゴリズム」のハッシュ値は -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 アルゴリズム\";\nint hashStr = str.GetHashCode();\n// 文字列「Hello アルゴリズム」のハッシュ値は -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 アルゴリズム\"\nlet hashStr = str.hashValue\n// 文字列「Hello アルゴリズム」のハッシュ値は -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 アルゴリズム\";\nint hashStr = str.hashCode;\n// 文字列「Hello アルゴリズム」のハッシュ値は 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 アルゴリズム\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 文字列「Hello アルゴリズム」のハッシュ値は 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 アルゴリズム\"\nval hashStr = str.hashCode()\n// 文字列「Hello アルゴリズム」のハッシュ値は -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 アルゴリズム\"\nhash_str = str.hash\n# 文字列「Hello アルゴリズム」のハッシュ値は -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%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%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":"

    元のハッシュテーブルでは、各バケットには 1 つのキーと値のペアしか格納できません。チェイン法(separate chaining)では、単一要素を連結リストに置き換え、キーと値のペアを連結リストのノードとして扱い、衝突したすべてのキーと値のペアを同じ連結リストに格納します。下図はチェイン法によるハッシュテーブルの例を示しています。

    図 6-5   チェイン法ハッシュテーブル

    チェイン法で実装されたハッシュテーブルでは、操作方法が次のように変わります。

    • 要素の検索:入力 key をハッシュ関数に通してバケットインデックスを得ると、連結リストの先頭ノードにアクセスできます。その後、連結リストを走査して key を比較し、目的のキーと値のペアを探します。
    • 要素の追加:まずハッシュ関数で連結リストの先頭ノードにアクセスし、その後ノード(キーと値のペア)を連結リストに追加します。
    • 要素の削除:ハッシュ関数の結果に基づいて連結リストの先頭にアクセスし、続いて連結リストを走査して対象ノードを探し、削除します。

    チェイン法には次の制約があります。

    • 使用メモリの増加:連結リストにはノードポインタが含まれるため、配列よりも多くのメモリを消費します。
    • 検索効率の低下:対応する要素を見つけるために連結リストを線形走査する必要があるためです。

    以下のコードはチェイン法ハッシュテーブルの簡単な実装を示しています。注意すべき点は 2 つあります。

    • 連結リストの代わりにリスト(動的配列)を使って、コードを簡潔にしています。この設定では、ハッシュテーブル(配列)は複数のバケットを含み、各バケットは 1 つのリストです。
    • 以下の実装にはハッシュテーブルの拡張メソッドが含まれています。負荷率が \\(\\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 を返します。

    下図はオープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布を示しています。このハッシュ関数では、末尾 2 桁が同じ key はすべて同じバケットに写像されます。線形探索によって、それらはそのバケットとその後続のバケットに順に格納されます。

    図 6-6   オープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布

    しかし、**線形探索では「クラスタリング現象」が起こりやすい**です。具体的には、配列内で連続して占有された位置が長いほど、それらの連続位置でハッシュ衝突が発生する可能性が高くなり、さらにその位置の集積成長を促して悪循環を生み、最終的には追加・削除・検索・更新操作の効率低下を招きます。

    注意すべきなのは、**オープンアドレッシングハッシュテーブルでは要素を直接削除できない**ことです。これは、要素を削除すると配列内に空バケット None が生じ、要素を検索するときに線形探索がその空バケットに到達した時点で返ってしまうため、その空バケットより後ろの要素には二度とアクセスできなくなるからです。結果として、プログラムがそれらの要素を存在しないと誤判定する可能性があります。下図のとおりです。

    図 6-7   オープンアドレッシングで要素を削除したことによる検索問題

    この問題を解決するために、遅延削除(lazy deletion)の仕組みを採用できます。これは要素をハッシュテーブルから直接取り除かず、代わりに定数 TOMBSTONE を使ってこのバケットをマークします。この仕組みでは、NoneTOMBSTONE はどちらも空バケットを表し、どちらにもキーと値のペアを配置できます。ただし異なるのは、線形探索が TOMBSTONE に到達した場合は、その先にキーと値のペアが存在する可能性があるため、探索を続けるべきだという点です。

    しかし、遅延削除はハッシュテーブルの性能劣化を加速させる可能性があります。これは、削除操作のたびに削除マークが 1 つ生成され、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 つです。衝突が発生したとき、二次探索では単純に固定歩数を飛ばすのではなく、「探索回数の二乗」に相当する歩数、すなわち \\(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 個のキーと値のペアを格納でき、容量を超えるとオーバーフローバケットを連結します。オーバーフローバケットが多すぎる場合は、性能を確保するために特殊な等量拡張操作を実行します。
    ","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   ハッシュテーブル","text":"

    ハッシュテーブル(hash table)は、散列表とも呼ばれ、キー key と値 value の対応関係を構築することで、高効率な要素検索を実現します。具体的には、ハッシュテーブルにキー key を入力すると、対応する値 value を \\(O(1)\\) 時間で取得できます。

    以下の図に示すように、\\(n\\) 人の学生がいるとし、各学生は「名前」と「学籍番号」の 2 つの情報を持っています。もし「学籍番号を入力すると対応する名前を返す」という検索機能を実現したいなら、下図のようなハッシュテーブルを用いることができます。

    図 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('\\n10583 を削除した後のハッシュテーブル\\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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%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

    ハッシュテーブルには、キーと値のペア、キー、値を走査する 3 つの一般的な方法があります。コード例は以下のとおりです:

    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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%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%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%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":"

    まずは最も単純なケースとして、**1 つの配列だけでハッシュテーブルを実装する**ことを考えます。ハッシュテーブルでは、配列中の各空き位置をバケット(bucket)と呼び、各バケットには 1 つのキーと値のペアを格納できます。したがって、検索操作とは key に対応するバケットを見つけ、そのバケットから value を取得することです。

    では、key に基づいて対応するバケットをどのように特定するのでしょうか。これはハッシュ関数(hash function)によって実現されます。ハッシュ関数の役割は、大きな入力空間をより小さな出力空間に写像することです。ハッシュテーブルでは、入力空間はすべての key 、出力空間はすべてのバケット(配列インデックス)です。言い換えると、key を入力すると、ハッシュ関数によってその key に対応するキーと値のペアの配列内での格納位置を求められます。

    key を入力したとき、ハッシュ関数の計算過程は次の 2 段階に分かれます。

    1. あるハッシュアルゴリズム hash() を用いてハッシュ値を計算します。
    2. ハッシュ値をバケット数(配列長)capacity で剰余し、その key に対応するバケット(配列インデックス)index を求めます。
    index = hash(key) % capacity\n

    その後、index を使ってハッシュテーブル内の対応するバケットにアクセスし、value を取得できます。

    配列長を capacity = 100 、ハッシュアルゴリズムを hash(key) = key とすると、ハッシュ関数は key % 100 となります。次の図では、key を学籍番号、value を名前の例として、ハッシュ関数の動作原理を示します。

    図 6-2   ハッシュ関数の動作原理

    以下のコードは、単純なハッシュテーブルを実装したものです。ここでは、キーと値のペアを表すために keyvalue をクラス 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 の下 2 桁が同じであれば、出力結果も同じになります。たとえば、学籍番号 12836 と 20336 の 2 人の学生を検索すると、次の結果を得ます:

    12836 % 100 = 36\n20336 % 100 = 36\n

    次の図に示すように、2 つの学籍番号が同じ名前を指してしまっており、これは明らかに誤りです。このような、複数の入力が同じ出力に対応する状況をハッシュ衝突(hash collision)と呼びます。

    図 6-3   ハッシュ衝突の例

    容易に分かるように、ハッシュテーブルの容量 \\(n\\) が大きいほど、複数の key が同じバケットに割り当てられる確率は低くなり、衝突も少なくなります。したがって、ハッシュテーブルを拡張することでハッシュ衝突を減らせます。

    次の図に示すように、拡張前はキーと値のペア (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 を入力すると、ハッシュテーブルは \\(O(1)\\) 時間で value を検索でき、非常に高効率である。
    • 一般的なハッシュテーブルの操作には、検索、キーと値のペアの追加、キーと値のペアの削除、ハッシュテーブルの走査などがある。
    • ハッシュ関数は key を配列インデックスに写像し、それによって対応するバケットにアクセスして value を取得する。
    • 異なる 2 つの key が、ハッシュ関数を通した後に同じ配列インデックスになることがあり、検索結果の誤りを引き起こす。この現象をハッシュ衝突と呼ぶ。
    • ハッシュテーブルの容量が大きいほど、ハッシュ衝突の確率は低くなる。そのため、ハッシュテーブルを拡張することでハッシュ衝突を緩和できる。配列の拡張と同様に、ハッシュテーブルの拡張操作のコストは大きい。
    • 負荷率は、ハッシュテーブル内の要素数をバケット数で割ったものと定義され、ハッシュ衝突の深刻さを反映する。ハッシュテーブル拡張を発動する条件としてよく用いられる。
    • 連鎖方式では、単一要素を連結リストに変換し、衝突したすべての要素を同じ連結リストに格納する。しかし、連結リストが長すぎると検索効率が低下するため、さらに連結リストを赤黒木に変換して効率を高めることができる。
    • オープンアドレス法は複数回の探索によってハッシュ衝突を処理する。線形探索は固定のステップ幅を用いるが、要素を削除できず、クラスタリングが発生しやすいという欠点がある。二重ハッシュは複数のハッシュ関数を用いて探索するため、線形探索に比べてクラスタリングが起きにくいが、複数のハッシュ関数によって計算量が増える。
    • プログラミング言語ごとに、異なるハッシュテーブル実装が採用されている。たとえば、Java の HashMap は連鎖方式を使用し、Python の Dict はオープンアドレス法を採用している。
    • ハッシュテーブルでは、ハッシュアルゴリズムに決定性、高効率、均一分布という特徴が求められる。暗号学では、ハッシュアルゴリズムはさらに耐衝突性とアバランシェ効果も備えるべきである。
    • ハッシュアルゴリズムは通常、大きな素数を法として用い、ハッシュ値の均一分布を最大限に保証してハッシュ衝突を減らす。
    • 一般的なハッシュアルゴリズムには 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 章   ヒープ"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"章の内容","text":"
    • 8.1   ヒープ
    • 8.2   ヒープ構築
    • 8.3   Top-k 問題
    • 8.4   まとめ
    ","path":["第 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":"

    まず空のヒープを作成し、次にリストを走査して、各要素に対して順に「ヒープへの挿入操作」を実行します。つまり、要素をヒープの末尾に追加してから、その要素に対して「下から上へ」のヒープ化を行います。

    要素が1つヒープに挿入されるたびに、ヒープの長さは1増加します。ノードは上から下へ順に二分木へ追加されるため、ヒープは「上から下へ」構築されます。

    要素数を \\(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":"

    実際には、より効率的なヒープ構築法を実現でき、全体は2つの手順に分かれます。

    1. リストのすべての要素をそのままヒープに追加します。この時点では、ヒープの性質はまだ満たされていません。
    2. ヒープを逆順で走査し(レベル順走査の逆順)、各非葉ノードに対して順に「上から下へ」のヒープ化を実行します。

    あるノードをヒープ化するたびに、そのノードを根とする部分木は合法な部分ヒープになります。また、逆順で走査するため、ヒープは「下から上へ」構築されます。

    逆順走査を選ぶのは、この方法なら現在のノードの下にある部分木がすでに合法な部分ヒープであることを保証でき、そのうえで現在のノードをヒープ化してはじめて有効になるからです。

    なお、葉ノードには子ノードがないため、それ自体が自然に合法な部分ヒープであり、ヒープ化は不要です。以下のコードが示すように、最後の非葉ノードは最後のノードの親ノードであり、そこから逆順に走査してヒープ化を実行します。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
    def __init__(self, nums: list[int]):\n    \"\"\"コンストラクタ。入力リストに基づいてヒープを構築する\"\"\"\n    # リスト要素をそのままヒープに追加\n    self.max_heap = nums\n    # 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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        // 葉ノード以外のすべてのノードをヒープ化\n        h.siftDown(i)\n    }\n    return h\n}\n
    my_heap.swift
    /* コンストラクタ。入力リストに基づいてヒープを構築する */\ninit(nums: [Int]) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = nums\n    // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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  // 葉ノード以外のすべてのノードをヒープ化\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    // 葉ノード以外のすべてのノードをヒープ化\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        // 葉ノード以外のすべてのノードをヒープ化\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        // 葉ノード以外のすべてのノードをヒープ化\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            // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // 2 つのノードを交換\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            // 2 つのノードを交換\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  # 葉ノード以外のすべてのノードをヒープ化\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":"

    以下では、2つ目のヒープ構築法の時間計算量を求めてみましょう。

    • 完全二分木のノード数を \\(n\\) とすると、葉ノード数は \\((n + 1) / 2\\) です。ここで \\(/\\) は切り捨て除算を表します。したがって、ヒープ化が必要なノード数は \\((n - 1) / 2\\) です。
    • 上から下へのヒープ化の過程では、各ノードは最大で葉ノードまでヒープ化されるため、最大反復回数は二分木の高さ \\(\\log n\\) です。

    上の2つを掛け合わせると、ヒープ構築過程の時間計算量は \\(O(n \\log n)\\) となります。しかし、この見積もりは正確ではありません。二分木では下層のノード数が上層よりはるかに多いという性質を考慮していないためです。

    次に、より正確な計算を行います。計算を簡単にするため、ノード数が \\(n\\) 、高さが \\(h\\) の「満二分木」を仮定します。この仮定は計算結果の正しさに影響しません。

    図 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)は、特定の条件を満たす完全二分木であり、主に次の 2 種類に分けられます。

    • 最小ヒープ(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   ヒープの操作効率

    メソッド名 説明 時間計算量 push() 要素をヒープに追加 \\(O(\\log n)\\) pop() ヒープ頂点の要素を取り出す \\(O(\\log n)\\) peek() ヒープ頂点の要素にアクセス(最大 / 最小ヒープではそれぞれ最大 / 最小値) \\(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# Python の heapq モジュールはデフォルトで最小ヒープを実装している\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// 最大ヒープを初期化(lambda 式で Comparator を変更すればよい)\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// 最大ヒープを初期化(lambda 式で Comparer を変更すればよい)\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// Swift の 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// 最大ヒープを初期化(lambda 式で Comparator を変更すればよい)\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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20min_heap,%20flag%20%3D%20%5B%5D,%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap,%20flag%20%3D%20%5B%5D,%20-1%0A%20%20%20%20%0A%20%20%20%20%23%20Python%20%E7%9A%84%20heapq%20%E6%A8%A1%E5%9D%97%E9%BB%98%E8%AE%A4%E5%AE%9E%E7%8E%B0%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E8%80%83%E8%99%91%E5%B0%86%E2%80%9C%E5%85%83%E7%B4%A0%E5%8F%96%E8%B4%9F%E2%80%9D%E5%90%8E%E5%86%8D%E5%85%A5%E5%A0%86%EF%BC%8C%E8%BF%99%E6%A0%B7%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%B0%86%E5%A4%A7%E5%B0%8F%E5%85%B3%E7%B3%BB%E9%A2%A0%E5%80%92%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%AE%9E%E7%8E%B0%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E5%9C%A8%E6%9C%AC%E7%A4%BA%E4%BE%8B%E4%B8%AD%EF%BC%8Cflag%20%3D%201%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%B0%8F%E9%A1%B6%E5%A0%86%EF%BC%8Cflag%20%3D%20-1%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%201%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%203%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%202%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%205%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%204%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20flag%20*%20max_heap%5B0%5D%20%23%205%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%0A%20%20%20%20%23%20%E5%87%BA%E5%A0%86%E5%85%83%E7%B4%A0%E4%BC%9A%E5%BD%A2%E6%88%90%E4%B8%80%E4%B8%AA%E4%BB%8E%E5%A4%A7%E5%88%B0%E5%B0%8F%E7%9A%84%E5%BA%8F%E5%88%97%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%205%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%204%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%203%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%202%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%201%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%0A%20%20%20%20size%20%3D%20len%28max_heap%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%A0%86%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20not%20max_heap%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%B9%B6%E5%BB%BA%E5%A0%86%0A%20%20%20%20min_heap%20%3D%20%5B1,%203,%202,%205,%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":"

    「二分木」の章で述べたように、完全二分木は配列で表現するのに非常に適しています。ヒープはまさに完全二分木の一種なので、ここでは配列を使ってヒープを格納します。

    配列で二分木を表す場合、要素はノードの値を表し、インデックスは二分木におけるノードの位置を表します。ノード間の参照関係はインデックスの対応式によって実現できます。

    次の図に示すように、インデックス \\(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 がヒープ内のほかの要素より大きい可能性があるため、ヒープ条件が崩れているかもしれません。そのため、挿入ノードから根ノードまでの経路上にある各ノードを修復する必要があります。この操作をヒープ化(heapify)と呼びます。

    ヒープへ追加したノードから始めて、**下から上へヒープ化**を行います。次の図のように、挿入ノードとその親ノードの値を比較し、挿入ノードのほうが大きければそれらを交換します。その後もこの操作を繰り返し、下から上へ各ノードを修復して、根ノードを越えるか交換不要のノードに達した時点で終了します。

    <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        # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // 2 つのノードを交換\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    // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // 2 つのノードを交換\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        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // 2 つのノードを交換\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    # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # 2 つのノードを交換\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. 根ノードから開始し、**上から下へヒープ化**を行う。

    次の図のように、**「上から下へのヒープ化」の方向は「下から上へのヒープ化」と逆**です。根ノードの値を 2 つの子ノードと比較し、最大の子ノードと根ノードを交換します。その後、この操作を繰り返し、葉ノードを越えるか交換不要のノードに達した時点で終了します。

    <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        # 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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    // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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        // 2 つのノードを交換\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    # 2 つのノードを交換\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

    長さ \\(n\\) の未整列配列 nums が与えられたとき、配列内で最大の \\(k\\) 個の要素を返してください。

    この問題について、まずは発想が比較的直接的な 2 つの解法を紹介し、その後でより効率の高いヒープ解法を紹介します。

    ","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#831","level":2,"title":"8.3.1   方法一:走査による選択","text":"

    以下の図に示すように \\(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","level":2,"title":"8.3.2   方法二:ソート","text":"

    以下の図に示すように、まず配列 nums をソートし、その後で右端の \\(k\\) 個の要素を返すことができます。時間計算量は \\(O(n \\log n)\\) です。

    明らかに、この方法は必要以上の処理を行っています。なぜなら、必要なのは最大の \\(k\\) 個の要素を見つけることだけであり、他の要素をソートする必要はないからです。

    図 8-7   ソートによって最大の k 個の要素を探す

    ","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#833","level":2,"title":"8.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 中国版で「剣指 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. ピンインの先頭文字が \\(r\\) のページを見つけるまで、手順 1. と手順 2. を繰り返します。
    <1><2><3><4><5>

    図 1-1   辞書を引く手順

    辞書を引くという小学生の必須スキルは、実は有名な「二分探索」アルゴリズムそのものです。データ構造の観点では、辞書を整列済みの「配列」とみなせます。アルゴリズムの観点では、上記の一連の辞書引きの操作を「二分探索」とみなせます。

    例2:トランプを整理する。カードゲームをするとき、毎回手札のトランプを小さい順に並べ替える必要があります。その流れは次の図のとおりです。

    1. トランプを「整列済み」と「未整列」の2つの部分に分け、初期状態では一番左の1枚がすでに整列済みだとします。
    2. 未整列部分から1枚のトランプを取り出し、整列済み部分の正しい位置に挿入します。完了すると、左端の2枚は整列済みになります。
    3. 手順 2. を繰り返し、各ラウンドで未整列部分から1枚を整列済み部分へ挿入し、すべてのトランプが整列済みになるまで続けます。

    図 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)とは、データを整理して保存する方式であり、データの内容、データ間の関係、データの操作方法を含み、次のような設計目標があります。

    • 使用する空間をできるだけ少なくし、コンピュータのメモリを節約します。
    • データの操作をできるだけ高速にし、アクセス、追加、削除、更新などを含みます。
    • 簡潔なデータ表現と論理情報を提供し、アルゴリズムが効率よく動作できるようにします。

    データ構造の設計はトレードオフに満ちた過程です。ある面を改善したい場合、別の面で妥協が必要になることがよくあります。以下に 2 つの例を示します。

    • 連結リストは配列に比べてデータの追加や削除がしやすい一方で、データアクセス速度を犠牲にしています。
    • グラフは連結リストに比べてより豊富な論理情報を提供しますが、より大きなメモリ空間を必要とします。
    ","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123","level":2,"title":"1.2.3   データ構造とアルゴリズムの関係","text":"

    以下の図のように、データ構造とアルゴリズムは高度に関連し、密接に結び付いており、具体的には次の 3 つの点に表れます。

    • データ構造はアルゴリズムの土台です。データ構造はアルゴリズムに対して、構造化して格納されたデータと、そのデータを操作する方法を提供します。
    • アルゴリズムはデータ構造に命を吹き込みます。データ構造そのものはデータ情報を保存するだけであり、アルゴリズムと組み合わせて初めて特定の問題を解決できます。
    • アルゴリズムは通常、異なるデータ構造に基づいて実装できますが、実行効率が大きく異なる場合があり、適切なデータ構造を選ぶことが重要です。

    図 1-4   データ構造とアルゴリズムの関係

    データ構造とアルゴリズムは、以下の図に示す組み立てブロックのようなものです。1 セットのブロックには多くの部品が含まれるだけでなく、詳しい組み立て説明書も付いています。説明書に従って一歩ずつ操作すれば、精巧なブロック模型を組み立てられます。

    図 1-5   組み立てブロック

    両者の詳細な対応関係を次の表に示します。

    表 1-1   データ構造とアルゴリズムを組み立てブロックにたとえる

    データ構造とアルゴリズム 組み立てブロック 入力データ まだ組み立てていないブロック データ構造 ブロックの構成形式。形状、大きさ、接続方法などを含む アルゴリズム ブロックを目標の形に組み上げる一連の操作手順 出力データ ブロック模型

    特筆すべき点として、データ構造とアルゴリズムはプログラミング言語から独立しています。だからこそ、本書では複数のプログラミング言語に基づく実装を提供できます。

    慣習的な略称

    実際の議論では、私たちは通常「データ構造とアルゴリズム」を略して「アルゴリズム」と呼びます。たとえば広く知られている LeetCode のアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識を同時に問うています。

    ","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_paperbook/","level":1,"title":"紙の書籍","text":"

    長い時間をかけて磨き上げた『Hello アルゴリズム』の紙の書籍が、ついに発売されました!今の気持ちは、次の一節で表せます:

    風を追い月を追って立ち止まるな、草原の果てには春の山がある。

    以下の動画では紙の書籍を紹介しており、私の考えもいくつか含まれています:

    • データ構造とアルゴリズムを学ぶ重要性。
    • なぜ紙の書籍で Python を選んだのか。
    • 知識共有に対する理解。

    新人 UP 主ですので、ぜひ応援と高評価・チャンネル登録をお願いします~ありがとうございます!

    紙の書籍のスナップショット:

    ","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_2","level":2,"title":"長所と短所","text":"

    紙の書籍ならではの魅力を、簡単にまとめると次のとおりです:

    • フルカラー印刷を採用し、本書の「アニメーション図解」の強みをそのまま活かせます。
    • 紙の素材にもこだわり、色彩を高い精度で再現しつつ、紙の書籍ならではの質感も残しています。
    • 紙の書籍版は Web 版よりも書式が整っており、たとえば図中の数式には斜体を用いています。
    • 価格を上げずに、マインドマップの折り込みページやしおりも付属します。
    • 紙の書籍、Web 版、PDF 版で内容は同期しており、自由に切り替えて読めます。

    Tip

    紙の書籍と Web 版を同期させるのは難しいため、細かな違いが生じる場合があります。ご了承ください!

    もちろん、購入前に検討しておくべき点もいくつかあります:

    • Python 言語を使用しているため、あなたの主言語と合わない可能性があります(Python は疑似コードと捉え、考え方の理解を重視してください)。
    • フルカラー印刷は図解やコードの読みやすさを大きく高める一方で、白黒印刷より価格はやや高くなります。

    Tip

    「印刷品質」と「価格」は、アルゴリズムにおける「時間効率」と「空間効率」のようなもので、両立は容易ではありません。そして私は、「印刷品質」は「時間効率」に当たるため、より重視すべきだと考えています。

    ","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_3","level":2,"title":"購入リンク","text":"

    紙の書籍に興味があれば、ぜひ一冊ご検討ください。新刊の 5 割引を用意していただきましたので、こちらのリンクをご覧いただくか、以下の QR コードをスキャンしてください:

    ","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_4","level":2,"title":"あとがき","text":"

    当初、私は紙の書籍出版に必要な作業量を甘く見ていて、オープンソースプロジェクトをきちんと保守していれば、紙の書籍版も何らかの自動化手段で生成できると思っていました。実際には、紙の書籍の制作フローとオープンソースプロジェクトの更新の仕組みには大きな違いがあり、その間をつなぐには多くの追加作業が必要でした。

    一冊の本の初稿と出版基準を満たす完成稿との間には、なお大きな隔たりがあります。出版社(企画、編集、デザイン、マーケティングなど)と著者が力を合わせ、長い時間をかけて磨き上げていく必要があります。ここで、図霊の企画編集者である王軍花さん、そして人民郵電出版社と図霊コミュニティで本書の出版工程に携わってくださったすべての皆さまに感謝いたします!

    この本があなたの助けになれば幸いです!

    ","path":["紙の書籍"],"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   本書の主な内容

    ","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 によって開発されました。彼の貢献に感謝します。読者により柔軟な読書方法を提供してくれました。

    本書の執筆過程で、私は多くの方々の助けを得ました。

    • 会社での私の指導教員である李汐博士に感謝します。ある対話の中で「すぐに行動しよう」と励ましてくださり、この本を書く決意を固めることができました;
    • 私の恋人であり、本書の最初の読者でもある泡泡に感謝します。アルゴリズム初心者の視点から多くの貴重な提案をしてくれたおかげで、本書はより初心者に適したものになりました;
    • 腾宝、琦宝、飞宝が本書に創造性あふれる名前を付けてくれたことに感謝します。みんなが最初のコード行「Hello World!」を書いた美しい記憶を呼び起こしてくれました;
    • 校铨が知的財産の面で専門的な支援をしてくれたことに感謝します。これは本オープンソース書籍の改善に重要な役割を果たしました;
    • 苏潼が本書の美しい表紙と logo をデザインし、私の完璧主義につき合って何度も辛抱強く修正してくれたことに感謝します;
    • @squidfunk が組版に関する助言を提供してくれたこと、そして彼が開発したオープンソースのドキュメントテーマ Material-for-MkDocs に感謝します。

    執筆の過程で、私はデータ構造とアルゴリズムに関する多くの教材や記事を読みました。これらの作品は本書に優れた手本を与え、本書の内容の正確性と品質を支えてくれました。ここに、すべての先生方と先人たちの卓越した貢献に感謝します!

    本書は手と頭を同時に使う学習方法を提唱しています。この点で私は『手を動かして学ぶ深層学習』から大きな啓発を受けました。ここで読者の皆さんにこの優れた著作を強くお勧めします。

    心から両親に感謝します。いつも支え励ましてくれたからこそ、私はこの興味深いことに取り組む機会を得ることができました。

    ","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 版)または下線付き(Web 版)で示します。たとえば配列(array)のようなものです。文献を読む際に役立つため、覚えておくことをおすすめします。
    • 重要な内容やまとめの文は 太字 で示します。これらの文章には特に注意してください。
    • 特定の意味を持つ語句には“引用符”を付け、曖昧さを避けます。
    • プログラミング言語ごとに用語が一致しない場合、本書では Python を基準とします。たとえば、“空”を表すのに None を使います。
    • 本書では、よりコンパクトなレイアウトのために、言語ごとのコメント規約を一部省略しています。コメントは主に3種類あります。タイトルコメント、内容コメント、複数行コメントです。
    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   アニメーション図解の例

    ","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#023","level":2,"title":"0.2.3   コード実践で理解を深める","text":"

    本書のサンプルコードは GitHub リポジトリ で管理されています。以下の図のように、ソースコードにはテストケースが付いており、ワンクリックで実行できます。

    時間に余裕があれば、コードを見ながら自分で一度書いてみることをおすすめします。学習時間が限られている場合でも、少なくともすべてのコードに目を通し、実行してください。

    コードを読むのに比べて、書く過程のほうが得られるものは多いものです。手を動かしてこそ、本当に学んだことになります。

    図 0-3   コード実行例

    コードを実行する前準備は主に3ステップです。

    第1ステップ:ローカルのプログラミング環境をインストールする。付録のチュートリアルを参照してインストールしてください。すでにインストール済みであれば、この手順は省略できます。

    第2ステップ:コードリポジトリをクローンまたはダウンロードする。 GitHub リポジトリ にアクセスしてください。すでに Git をインストールしている場合は、次のコマンドでこのリポジトリをクローンできます:

    git clone https://github.com/krahets/hello-algo.git\n

    もちろん、以下の図に示す場所で“Download ZIP”ボタンをクリックし、コードの圧縮ファイルを直接ダウンロードしてローカルで展開することもできます。

    図 0-4   リポジトリのクローンとコードのダウンロード

    第3ステップ:ソースコードを実行する。以下の図のように、上部にファイル名が表示されているコードブロックについては、リポジトリの codes フォルダ内に対応するソースコードファイルがあります。ソースコードファイルはワンクリックで実行できるため、不要なデバッグ時間を減らし、学習内容に集中できます。

    図 0-5   コードブロックと対応するソースコードファイル

    ローカルでコードを実行するだけでなく、Web 版では Python コードの可視化実行にも対応しています(pythontutor を利用)。以下の図のように、コードブロックの下にある“可視化実行”をクリックすると表示を展開し、アルゴリズムコードの実行過程を観察できます。また、“全画面表示”をクリックすると、より見やすい閲覧体験が得られます。

    図 0-6   Python コードの可視化実行

    ","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#024","level":2,"title":"0.2.4   質問と議論を通じてともに成長する","text":"

    本書を読んでいて、理解できていない知識点を安易に読み飛ばさないでください。コメント欄で気軽に質問してください。私と仲間たちが誠意をもって回答し、通常は 2 日以内に返信します。

    以下の図のように、Web 版では各章の下部にコメント欄があります。ぜひコメント欄の内容にも目を通してください。一方では、みんなが直面した問題を知ることで知識の抜けを補い、より深い思考を促せます。もう一方では、ほかの仲間の質問にも積極的に答え、見解を共有し、互いの成長を助けてほしいと思います。

    図 0-7   コメント欄の例

    ","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#025","level":2,"title":"0.2.5   アルゴリズム学習ロードマップ","text":"

    全体として見ると、データ構造とアルゴリズムの学習過程は 3 つの段階に分けられます。

    1. 第 1 段階:アルゴリズム入門。さまざまなデータ構造の特徴と使い方に慣れ、異なるアルゴリズムの原理、流れ、用途、効率などを学ぶ必要があります。
    2. 第 2 段階:アルゴリズム問題を解く。まずは人気の高い問題から取り組み、少なくとも 100 問は蓄積して、主流のアルゴリズム問題に慣れることをおすすめします。最初のうちは、“知識の忘却”が課題になるかもしれませんが、心配はいりません。これはごく自然なことです。“エビングハウスの忘却曲線”に沿って問題を復習すれば、通常は 3~5 回繰り返すことでしっかり記憶に定着します。おすすめの問題リストと学習計画は、この GitHub リポジトリ を参照してください。
    3. 第 3 段階:知識体系を構築する。学習面では、アルゴリズムの連載記事、解法フレームワーク、教材などを読むことで、知識体系を継続的に充実させられます。問題演習の面では、トピック別分類、1 問多解、1 解多題といった発展的な戦略も試せます。関連する学習ノウハウは各コミュニティで見つけられます。

    以下の図のように、本書の内容は主に“第 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":"
    • 本書の主な対象読者はアルゴリズム初学者です。すでにある程度の基礎がある場合でも、本書はアルゴリズム知識を体系的に振り返る助けとなり、書中のソースコードは「問題演習用ツール集」としても利用できます。
    • 本書の内容は主に計算量解析、データ構造、アルゴリズムの三部からなり、この分野の大部分のテーマを網羅しています。
    • アルゴリズム初心者にとって、学習初期の段階で入門書を読むことは非常に重要であり、多くの遠回りを避けられます。
    • 本書のアニメーション図解は通常、重要な知識や難しい知識を紹介するために用いられます。本書を読む際は、これらの内容により多く注意を払うべきです。
    • 実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、実際に自分でコードを書くことを強く勧めます。
    • 本書のWeb版の各章にはコメント欄が設けられており、疑問や見解をいつでも共有することを歓迎します。
    ","path":["第 0 章   前書き","0.3   まとめ"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"参考文献","text":"

    [1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).

    [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

    [3] Robert Sedgewick, et al. Algorithms (4th Edition).

    [4] 严蔚敏. データ構造(C 言語版).

    [5] 邓俊辉. データ構造(C++ 言語版、第3版).

    [6] マーク・アレン・ワイス著,陈越訳. データ構造とアルゴリズム解析:Java言語による記述(第3版).

    [7] 程杰. データ構造の話.

    [8] 王争. データ構造とアルゴリズムの美.

    [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

    [10] Aston Zhang, et al. Dive into Deep Learning.

    ","path":["参考文献"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章   探索","text":"

    Abstract

    探索は未知の冒険であり、私たちは神秘的な空間の隅々まで歩き回る必要があるかもしれず、あるいは素早く目標を特定できるかもしれません。

    この探索の旅において、すべての探求が思いもよらなかった答えをもたらすかもしれません。

    ","path":["第 10 章   探索"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"章の内容","text":"
    • 10.1   二分探索
    • 10.2   二分探索の挿入位置
    • 10.3   二分探索の境界
    • 10.4   ハッシュによる最適化戦略
    • 10.5   探索アルゴリズム再考
    • 10.6   まとめ
    ","path":["第 10 章   探索"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   二分探索","text":"

    二分探索(binary search)は分割統治法に基づく効率的な探索アルゴリズムです。データが整列済みである性質を利用し、各ラウンドで探索範囲を半分に縮小し、目標要素を見つけるか探索区間が空になるまで続けます。

    Question

    長さ \\(n\\) の配列 nums が与えられます。要素は小さい順に並んでおり、重複しません。要素 target がこの配列内にある場合はそのインデックスを返し、含まれない場合は \\(-1\\) を返してください。例を次の図に示します。

    図 10-1   二分探索の例

    次の図に示すように、まずポインタ \\(i = 0\\) と \\(j = n - 1\\) を初期化し、それぞれ配列の先頭要素と末尾要素を指すようにして、探索区間 \\([0, n - 1]\\) を表します。角括弧は閉区間を表し、境界値自体を含むことに注意してください。

    次に、以下の 2 つの手順を繰り返します。

    1. 中央のインデックス \\(m = \\lfloor {(i + j) / 2} \\rfloor\\) を計算します。ここで \\(\\lfloor \\: \\rfloor\\) は切り捨てを表します。
    2. nums[m]target の大小関係を判定し、次の 3 つの場合に分かれます。
      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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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 はそれぞれ配列の先頭要素と末尾要素+1を指す\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
    コードの可視化

    全画面で見る >

    次の図に示すように、2 種類の区間表現では、二分探索アルゴリズムの初期化、ループ条件、区間の縮小操作がそれぞれ異なります。

    「両閉区間」の表現では左右の境界がどちらも閉区間として定義されるため、ポインタ \\(i\\) とポインタ \\(j\\) による区間縮小の操作も対称になります。このほうがミスをしにくいため、一般には「両閉区間」の書き方を推奨します。

    図 10-3   2 種類の区間定義

    ","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 回の除算、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

    長さ \\(n\\) のソート済み配列 nums が与えられ、その中には重複要素が含まれる可能性があります。配列内で最も左にある要素 target のインデックスを返してください。配列にこの要素が含まれない場合は、\\(-1\\) を返します。

    二分探索で挿入位置を求める方法を思い出すと、探索完了後に \\(i\\) は最も左にある target を指します。したがって、挿入位置を探すことの本質は、最も左にある target のインデックスを探すことです。

    挿入位置を探す関数を使って左端境界を求めることを考えます。なお、配列に target が含まれない場合があり、そのときは次の 2 つの結果が起こりえます。

    • 挿入位置のインデックス \\(i\\) が範囲外になる。
    • 要素 nums[i]target と等しくない。

    上の 2 つの状況に当てはまる場合は、直接 \\(-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 の場合のポインタの縮小操作を置き換えることです。ここではコードを省略するので、興味があれば自分で実装してみてください。

    ここでは、より巧妙な 2 つの方法を紹介します。

    ","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1","level":3,"title":"1.   左端境界探索を再利用する","text":"

    実際には、最も左の要素を探す関数を利用して最も右の要素を探せます。具体的には、最も右にある target を探すことを、最も左にある target + 1 を探すことに変換します。

    下図のように、探索完了後、ポインタ \\(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 より小さい最初の要素を指すことになります。

    したがって、下図のように、配列中に存在しない要素を構成して、それを使って左右の境界を探せます。

    • 最も左にある target の探索:target - 0.5 を探すことに変換でき、ポインタ \\(i\\) を返します。
    • 最も右にある target の探索:target + 0.5 を探すことに変換でき、ポインタ \\(j\\) を返します。

    図 10-8   境界の探索を要素の探索に変換する

    ここではコードを省略しますが、次の 2 点に注意が必要です。

    • 与えられた配列には小数が含まれないため、等しい場合をどう処理するかを気にする必要はありません。
    • この方法では小数を導入するため、関数内の変数 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

    長さ \\(n\\) の整列済み配列 nums と要素 target が与えられます。配列には重複要素は存在しません。ここで target を配列 nums に挿入し、その順序を保ちます。配列中にすでに要素 target が存在する場合は、その左側に挿入します。挿入後の配列における target のインデックスを返してください。例を以下の図に示します。

    図 10-4   二分探索の挿入位置の例データ

    前節の二分探索コードを再利用したい場合は、次の二つの問題に答える必要があります。

    問題 1:配列に target が含まれる場合、挿入位置のインデックスはその要素のインデックスですか?

    問題では target を等しい要素の左側に挿入するよう求めているため、新しく挿入された target は元の target の位置に入ります。つまり、配列に target が含まれる場合、挿入位置のインデックスはその target のインデックスです。

    問題 2:配列に target が存在しない場合、挿入位置はどの要素のインデックスですか?

    二分探索の過程をさらに考えると、nums[m] < target のときは \\(i\\) が移動します。これは、ポインタ \\(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 があるかは分かりません。

    問題では目標要素を最も左に挿入する必要があるため、配列中で最も左にある target のインデックスを探す必要があります。まずは以下の図に示す手順で実現することを考えます。

    1. 二分探索を実行し、任意の target のインデックスを得て、これを \\(k\\) とします。
    2. インデックス \\(k\\) から始めて左へ線形探索し、最も左の target を見つけたら返します。

    図 10-5   線形探索による重複要素の挿入位置

    この方法は使用できますが、線形探索を含むため、時間計算量は \\(O(n)\\) です。配列中に重複した target が多い場合、この方法の効率は低くなります。

    次に、二分探索のコードを拡張することを考えます。以下の図に示すように、全体の流れは変えず、各反復でまず中点インデックス \\(m\\) を計算し、その後 targetnums[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] > targetnums[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 となる 2 つの要素を探索し,それらの配列インデックスを返してください。任意の 1 つの解を返せば十分です。

    ","path":["第 10 章   探索","10.4   ハッシュによる最適化戦略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041","level":2,"title":"10.4.1   線形探索:時間と引き換えに空間を節約","text":"

    考えられるすべての組み合わせを直接走査することを考えます。次の図に示すように,2 重ループを開始し,各ラウンドで 2 つの整数の和が target であるかを判定します。そうであれば,それらのインデックスを返します。

    図 10-9   線形探索で 2 数の和を求める

    コードは次のとおりです:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
    def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"方法 1:総当たり列挙\"\"\"\n    # 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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  // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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    // 2重ループのため、時間計算量は 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  # 2重ループのため、時間計算量は 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":"

    ハッシュテーブルを利用し,キーと値をそれぞれ配列要素と要素のインデックスにします。配列をループで走査し,各ラウンドで次の図に示す手順を実行します。

    1. 数値 target - nums[i] がハッシュテーブル内にあるかを判定します。あれば,この 2 つの要素のインデックスを直接返します。
    2. キーと値の組 nums[i] とインデックス i をハッシュテーブルに追加します。
    <1><2><3>

    図 10-10   補助ハッシュテーブルで 2 数の和を求める

    実装コードは次のとおりで,単一ループだけで済みます:

    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)は、データ構造(配列、連結リスト、木、グラフなど)の中から、特定の条件を満たす 1 つまたは複数の要素を探索するために用いられます。

    探索アルゴリズムは、実装の考え方に応じて次の 2 種類に分けられます。

    • データ構造を走査して目標要素を特定する方法。配列、連結リスト、木、グラフの走査などがこれに当たります。
    • データの構成やデータに含まれる事前情報を利用して、要素を効率よく探す方法。二分探索、ハッシュ探索、二分探索木による探索などがこれに当たります。

    これらのトピックはすでに前の章で扱っているため、探索アルゴリズムは私たちにとって見慣れたものです。本節では、より体系的な視点から探索アルゴリズムをあらためて見直します。

    ","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051","level":2,"title":"10.5.1   総当たり探索","text":"

    総当たり探索は、データ構造の各要素を順に調べて目標要素を特定します。

    • “線形探索”は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から始めて、要素を 1 つずつ調べ、目標要素が見つかるか、もう一方の端に達しても見つからないまで続けます。
    • “幅優先探索”と“深さ優先探索”は、グラフと木における 2 つの走査戦略です。幅優先探索は初期ノードから始めて層ごとに探索し、近いところから遠いところへ各ノードを訪れます。深さ優先探索は初期ノードから始めて 1 本の経路を最後までたどり、その後でバックトラックしてほかの経路を試し、データ構造全体を走査し終えるまで続けます。

    総当たり探索の利点は、単純で汎用性が高く、**データの前処理や追加のデータ構造を必要としない**ことです。

    しかし、この種のアルゴリズムの時間計算量は \\(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-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)\\) データの順序性 なし あり あり なし

    探索アルゴリズムの選択は、規模、探索性能の要求、データの問い合わせ頻度や更新頻度などにも左右されます。

    線形探索

    • 汎用性が高く、データの前処理をまったく必要としません。データを 1 回だけ問い合わせればよい場合、ほか 3 つの手法では前処理にかかる時間のほうが、線形探索そのものより長くなることがあります。
    • 規模の小さいデータに適しています。この場合、時間計算量が効率に与える影響は比較的小さいです。
    • データ更新頻度が高い場面に適しています。この手法では、データに対する追加の保守が不要だからです。

    二分探索

    • 大規模データに適しており、効率が安定しています。最悪時間計算量は \\(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 章   ソート"],"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 章   ソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   バブルソート","text":"

    バブルソート(bubble sort)は、隣接する要素を繰り返し比較して交換することで整列を行います。この過程が泡のように下から上へ浮かび上がることから、バブルソートと呼ばれます。

    次の図に示すように、バブル処理は要素の交換操作によってシミュレートできます。配列の最も左の端から右へ走査し、隣接する要素の大小を順に比較して、「左要素 > 右要素」であれば両者を交換します。走査が終わると、最大の要素は配列の最も右端へ移動します。

    <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\\) とすると、バブルソートの手順は次の図のとおりです。

    1. まず、\\(n\\) 個の要素に対して「バブル処理」を行い、配列中の最大要素を正しい位置へ交換します。
    2. 次に、残りの \\(n - 1\\) 個の要素に対して「バブル処理」を行い、2 番目に大きい要素を正しい位置へ交換します。
    3. このようにして、\\(n - 1\\) 回の「バブル処理」を終えると、大きいほうから \\(n - 1\\) 個の要素がすべて正しい位置へ交換されます。
    4. 残った 1 つの要素は必ず最小要素なので、並べ替える必要はなく、これで配列のソートが完了します。

    図 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)\\) の浮動小数点数であるとします。バケットソートの流れを以下の図に示します。

    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":"

    バケットソートは、非常に大規模なデータの処理に適しています。たとえば、入力データに 100 万個の要素が含まれ、空間の制約によりシステムメモリへすべてのデータを一度に読み込めない場合です。このとき、データを 1000 個のバケットに分け、それぞれのバケットを個別にソートしてから、最後に結果を結合できます。

    • 時間計算量は \\(O(n + k)\\) :要素が各バケット内に平均的に分布していると仮定すると、各バケット内の要素数は \\(\\frac{n}{k}\\) です。1 つのバケットをソートするのに \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) の時間がかかるなら、すべてのバケットのソートには \\(O(n \\log\\frac{n}{k})\\) の時間がかかります。バケット数 \\(k\\) が十分大きいとき、時間計算量は \\(O(n)\\) に近づきます。結果を結合する際には、すべてのバケットと要素を走査する必要があり、\\(O(n + k)\\) の時間を要します。最悪の場合、すべてのデータが 1 つのバケットに割り当てられ、そのバケットのソートに \\(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)\\) に達しますが、鍵は要素を各バケットへ均等に分配すること にあります。実際のデータは均一に分布していないことが多いからです。たとえば、Taobao 上のすべての商品を価格帯ごとに 10 個のバケットへ均等に分けたいとしても、商品の価格分布は偏っており、100 元未満は非常に多く、1000 元超は非常に少ないかもしれません。価格区間を単純に 10 等分すると、各バケットの商品数には大きな差が生じます。

    均等な分配を実現するために、まず大まかな境界線を設定し、データをひとまず 3 個のバケットに粗く振り分けます。分配後は、商品数の多いバケットをさらに 3 個のバケットに分割し、すべてのバケット内の要素数がおおむね等しくなるまでこれを続けます。

    以下の図に示すように、この方法の本質は再帰木を構築することにあり、目標は葉ノードの値をできるだけ均等にすることです。もちろん、毎回データを 3 個のバケットに分割する必要はなく、具体的な分け方はデータの特徴に応じて柔軟に選べます。

    図 11-14   再帰的にバケットを分割

    商品価格の確率分布をあらかじめ把握しているなら、データの確率分布に基づいて各バケットの価格境界を設定できます。なお、データ分布は必ずしも特別に統計を取る必要はなく、データの特徴に応じて何らかの確率モデルで近似することもできます。

    以下の図に示すように、商品価格が正規分布に従うと仮定すれば、価格区間を合理的に設定でき、それによって商品を各バケットへ均等に分配できます。

    図 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":"

    まず簡単な例を見てみましょう。長さ \\(n\\) の配列 nums が与えられ、その要素はすべて「非負整数」であるとします。計数ソートの全体的な流れを以下の図に示します。

    1. 配列を走査し、その中の最大値を見つけて \\(m\\) とし、続いて長さ \\(m + 1\\) の補助配列 counter を作成します。
    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 の各インデックスを 1 つのバケットとみなし、個数を数える過程を各要素を対応するバケットへ振り分ける操作とみなせます。本質的には、計数ソートは整数データにおけるバケットソートの特殊な一例です。

    ","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 に対して各反復で次の 2 つの手順を行います。

    1. num を配列 res のインデックス prefix[num] - 1 に格納します。
    2. 累積和 prefix[num] を \\(1\\) 減らし、次に num を配置するインデックスを得ます。

    走査が完了すると、配列 res にソート済みの結果が格納されます。最後に res で元の配列 nums を上書きすれば完了です。以下の図は完全な計数ソートの流れを示しています。

    <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    # 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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 は、res において _num が最後に出現する位置のインデックスである\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  // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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    // 結果配列 res で元の配列 nums を上書きする\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  # 結果配列 res で元の配列 nums を上書きする\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)\\)、非インプレースソート:長さがそれぞれ \\(n\\) と \\(m\\) の配列 rescounter を利用します。
    • 安定ソート: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\\) とすると、ヒープソートの流れは次図のとおりです。

    1. 配列を入力して最大ヒープを構築します。完了後、最大要素はヒープの頂点にあります。
    2. ヒープ頂点の要素(最初の要素)とヒープ末尾の要素(最後の要素)を交換します。交換後、ヒープの長さは \\(1\\) 減少し、整列済み要素数は \\(1\\) 増加します。
    3. ヒープ頂点の要素から始めて、上から下へヒープ化操作(sift down)を実行します。ヒープ化が完了すると、ヒープの性質が回復します。
    4. 2. ステップと第 3. ステップを繰り返し実行します。これを \\(n - 1\\) 回繰り返すと、配列の整列が完了します。

    Tip

    実際には、要素の取り出し操作にも第 2. ステップと第 3. ステップが含まれており、要素を取り出す処理が 1 つ加わるだけです。

    <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        # 2 つのノードを交換\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # ループで上から下へヒープ化\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"ヒープソート\"\"\"\n    # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        swap(nums[i], nums[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid heapSort(vector<int> &nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\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    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid HeapSort(int[] nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfunc heapSort(nums *[]int) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        nums.swapAt(i, ma)\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfunc heapSort(nums: inout [Int]) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfunction heapSort(nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfunction heapSort(nums: number[]): void {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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    // 2 つのノードを交換\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  // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\n        nums.swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfn heap_sort(nums: &mut [i32]) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\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    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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        // 2 つのノードを交換\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    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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    # 2 つのノードを交換\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # ループで上から下へヒープ化\n    i = ma\n  end\nend\n\n### ヒープソート ###\ndef heap_sort(nums)\n  # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\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)は単純なソートアルゴリズムであり、その動作原理は手作業でトランプの山を整える過程と非常によく似ています。

    具体的には、未ソート区間から基準要素を 1 つ選び、その要素を左側の整列済み区間の要素と 1 つずつ比較し、正しい位置に挿入します。

    以下の図は、配列に要素を挿入する操作の流れを示しています。基準要素を base とすると、目的のインデックスから base までのすべての要素を 1 つずつ右に移動し、その後 base を目的のインデックスに代入する必要があります。

    図 11-6   1 回の挿入操作

    ","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141","level":2,"title":"11.4.1   アルゴリズムの流れ","text":"

    挿入ソート全体の流れを以下の図に示します。

    1. 初期状態では、配列の 1 番目の要素はすでに整列済みです。
    2. 配列の 2 番目の要素を base として選び、正しい位置に挿入すると、**配列の先頭 2 要素が整列済み**になります。
    3. 3 番目の要素を base として選び、正しい位置に挿入すると、**配列の先頭 3 要素が整列済み**になります。
    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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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] を 1 つ右へ移動する\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)\\) ですが、実際には、挿入ソートはバブルソートや選択ソートよりもはるかに高い頻度で使われます。主な理由は次のとおりです。

    • バブルソートは要素の交換によって実装され、1 つの一時変数を必要とするため、合計で 3 回の基本演算が関わります。これに対して、挿入ソートは要素の代入に基づいており、必要な基本演算は 1 回だけです。したがって、バブルソートの計算コストは通常、挿入ソートより高くなります。
    • 選択ソートの時間計算量はどのような場合でも \\(O(n^2)\\) です。**部分的に整列されたデータが与えられた場合、挿入ソートは通常、選択ソートより効率的**です。
    • 選択ソートは安定ではないため、多段ソートには適用できません。
    ","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   マージソート","text":"

    マージソート(merge sort)は分割統治戦略に基づくソートアルゴリズムであり、以下の図に示す「分割」と「マージ」の段階から構成されます。

    1. 分割段階:再帰によって配列を中点で繰り返し分割し、長い配列のソート問題を短い配列のソート問題へ変換します。
    2. マージ段階:部分配列の長さが 1 になったら分割を終了し、マージを開始して、左右 2 つの短いソート済み配列をより長いソート済み配列へと繰り返しマージしていきます。

    図 11-10   マージソートの分割とマージの段階

    ","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161","level":2,"title":"11.6.1   アルゴリズムの流れ","text":"

    以下の図に示すように、「分割段階」では配列を上から下へ再帰的に中点で 2 つの部分配列へ分割します。

    1. 配列の中点 mid を計算し、左部分配列(区間 [left, mid] )と右部分配列(区間 [mid + 1, right] )を再帰的に分割します。
    2. 手順 1. を再帰的に実行し、部分配列区間の長さが 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)\\) まで最適化できます 。

    • 分割段階:連結リストの分割は「再帰」の代わりに「反復」で実装できるため、再帰で使用するスタックフレーム領域を省けます。
    • マージ段階:連結リストでは、ノードの追加や削除は参照(ポインタ)を変更するだけで実現できるため、マージ段階(2 つの短いソート済み連結リストを 1 つの長いソート済み連結リストにマージすること)では追加の連結リストを作成する必要がありません。

    具体的な実装の詳細は比較的複雑なので、興味のある読者は関連資料を参照して学習してください。

    ","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   クイックソート","text":"

    クイックソート(quick sort)は分割統治戦略に基づくソートアルゴリズムであり、実行効率が高く、広く利用されています。

    クイックソートの中核操作は「パーティション」であり、その目的は、配列内のある要素を「基準数」として選び、基準数より小さいすべての要素を左側へ、大きい要素を右側へ移動することです。具体的には、パーティションの流れを下図に示します。

    1. 配列の最左端の要素を基準数として選び、2 つのポインタ ij を初期化して、それぞれ配列の両端を指すようにします。
    2. ループを設定し、各ラウンドで ij)を使ってそれぞれ基準数より大きい(小さい)最初の要素を探し、その後この 2 つの要素を交換します。
    3. ij が出会うまでステップ 2. を繰り返し、最後に基準数を 2 つの部分配列の境界へ交換します。
    <1><2><3><4><5><6><7><8><9>

    図 11-8   パーティションの手順

    パーティションが完了すると、元の配列は左部分配列、基準数、右部分配列の 3 つに分けられ、「左部分配列の任意の要素 \\(\\leq\\) 基準数 \\(\\leq\\) 右部分配列の任意の要素」を満たします。したがって、次はこの 2 つの部分配列だけをソートすれば済みます。

    クイックソートの分割統治戦略

    パーティションの本質は、長い配列のソート問題を 2 つの短い配列のソート問題へ簡略化することです。

    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    # 基準値を 2 つの部分配列の境界へ交換する\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]); // この 2 つの要素を交換\n    }\n    swap(nums[i], nums[left]);  // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    Swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\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    // 基準値を 2 つの部分配列の境界へ交換する\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) // この 2 つの要素を交換\n    }\n    nums.swapAt(i, left) // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n  }\n  _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\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); // この 2 つの要素を交換\n    }\n    nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する\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        // この 2 つの要素を交換\n        swap(nums, i, j);\n    }\n    // 基準値を 2 つの部分配列の境界へ交換する\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)  // この 2 つの要素を交換\n    }\n    swap(nums, i, left)   // 基準値を 2 つの部分配列の境界へ交換する\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  # 基準値を 2 つの部分配列の境界へ交換する\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":"

    クイックソート全体の流れを下図に示します。

    1. まず、元の配列に対して 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\\) の 2 つの部分配列に分割され、このとき再帰の深さは \\(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)\\) の時間計算量で動作します。
    • キャッシュ利用効率が高い:パーティション操作の実行時には、システムが部分配列全体をキャッシュに読み込めるため、要素アクセスの効率が高くなります。一方、「ヒープソート」のようなアルゴリズムは要素へ飛び飛びにアクセスする必要があり、この性質を持ちません。
    • 計算量の定数係数が小さい:上記 3 つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作総数が最も少なくなります。これは「挿入ソート」が「バブルソート」より速い理由と似ています。
    ","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154","level":2,"title":"11.5.4   基準数の最適化","text":"

    クイックソートは、入力によっては時間効率が低下する可能性があります。極端な例として、入力配列が完全な逆順であるとします。最左端の要素を基準数として選ぶため、パーティション完了後には基準数が配列の最右端へ交換され、左部分配列の長さが \\(n - 1\\)、右部分配列の長さが \\(0\\) になります。この再帰を続けると、各回のパーティション後に必ず一方の部分配列の長さが \\(0\\) となり、分割統治戦略が機能せず、クイックソートは「バブルソート」に近い形へ退化します。

    この状況をできるだけ避けるため、パーティションにおける基準数の選び方を最適化できます。たとえば、ランダムに 1 つの要素を選んで基準数にできます。しかし、運が悪く毎回望ましくない基準数を選んでしまうと、効率は依然として十分ではありません。

    注意すべき点として、プログラミング言語が通常生成するのは「疑似乱数」です。疑似乱数列に合わせて特定のテストケースを構築すると、クイックソートの効率はやはり劣化する可能性があります。

    さらに改善するために、配列から 3 つの候補要素(通常は先頭、末尾、中間の要素)を選び、**その 3 つの候補要素の中央値を基準数とする**ことができます。こうすると、基準数が「小さすぎず大きすぎもしない」確率が大幅に上がります。もちろん、候補要素をさらに増やして、アルゴリズムの頑健性をいっそう高めることも可能です。この方法を採用すると、時間計算量が \\(O(n^2)\\) まで劣化する確率は大きく下がります。

    コード例を以下に示します。

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
    def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"3つの候補要素の中央値を選ぶ\"\"\"\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    \"\"\"番兵による分割処理(3 点中央値)\"\"\"\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    # 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # 基準値のインデックスを返す\n
    quick_sort.cpp
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint partition(vector<int> &nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\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]); // この 2 つの要素を交換\n    }\n    swap(nums[i], nums[left]);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;                   // 基準値のインデックスを返す\n}\n
    quick_sort.java
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint partition(int[] nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
    quick_sort.cs
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint Partition(int[] nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    Swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
    quick_sort.go
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\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    // 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // 基準値のインデックスを返す\n}\n
    quick_sort.swift
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // 3つの候補要素の中央値を選ぶ\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
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\npartition(nums, left, right) {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
    quick_sort.ts
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\npartition(nums: number[], left: number, right: number): number {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
    quick_sort.dart
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint _partition(List<int> nums, int left, int right) {\n  // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n  }\n  _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n  return i; // 基準値のインデックスを返す\n}\n
    quick_sort.rs
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    i // 基準値のインデックスを返す\n}\n
    quick_sort.c
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nint partitionMedian(int nums[], int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\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); // この 2 つの要素を交換\n    }\n    swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i;            // 基準値のインデックスを返す\n}\n
    quick_sort.kt
    /* 3つの候補要素の中央値を選ぶ */\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/* 番兵による分割処理(3 点中央値) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // 3つの候補要素の中央値を選ぶ\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)             // この 2 つの要素を交換\n    }\n    swap(nums, i, left)              // 基準値を 2 つの部分配列の境界へ交換する\n    return i                         // 基準値のインデックスを返す\n}\n
    quick_sort.rb
    ### 3 つの候補要素の中央値を選ぶ ###\ndef median_three(nums, left, mid, right)\n  # 3つの候補要素の中央値を選ぶ\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### 3 つの候補要素の中央値を選ぶ ###\ndef median_three(nums, left, mid, right)\n  # 3つの候補要素の中央値を選ぶ\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  # 基準値を 2 つの部分配列の境界へ交換する\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\\) の右部分配列が生成されます。これは、各再帰呼び出しで減る問題サイズが非常に小さいこと(要素が 1 つ減るだけ)を意味し、再帰木の高さは \\(n - 1\\) に達するため、このとき \\(O(n)\\) のスタックフレーム空間を占有します。

    スタックフレーム空間の蓄積を防ぐために、各回のパーティション完了後に 2 つの部分配列の長さを比較し、**短いほうの部分配列に対してのみ再帰**を行えます。短い部分配列の長さは \\(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        # 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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    // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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        // 2 つの部分配列のうち短いほうにクイックソートを適用する\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  # 基準値を 2 つの部分配列の境界へ交換する\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    # 2 つの部分配列のうち短いほうにクイックソートを適用する\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\\) 位としたとき、基数ソートの流れは次図のようになります。

    1. 桁番号 \\(k = 1\\) を初期化します。
    2. 学籍番号の第 \\(k\\) 位に対して「計数ソート」を実行します。完了すると、データは第 \\(k\\) 位に従って昇順に並びます。
    3. \\(k\\) を \\(1\\) 増やし、手順 2. に戻って反復を続けます。すべての桁のソートが完了したら終了します。

    図 11-18   基数ソートのアルゴリズムの流れ

    以下ではコード実装を分解して見ていきます。\\(d\\) 進数の数値 \\(x\\) について、その第 \\(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    \"\"\"要素 num の下から k 桁目を取得(exp = 10^(k-1))\"\"\"\n    # ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"計数ソート(nums の k 桁目でソート)\"\"\"\n    # 10 進数の各桁は 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)  # nums[i] の第 k 位を取得し、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  # d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint Digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunc digit(num, exp int) int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunc countingSortDigit(nums []int, exp int) {\n    // 10 進数の各桁は 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) // nums[i] の第 k 位を取得し、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 // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // 10 進数の各桁は 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) // nums[i] の第 k 位を取得し、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 // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunction digit(num, exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunction countingSortDigit(nums, exp) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunction digit(num: number, exp: number): number {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 `_num` の第 k 桁を取得する。ここで `exp = 10^(k-1)` */\nint digit(int _num, int exp) {\n  // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n  return (_num ~/ exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfn digit(num: i32, exp: i32) -> usize {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return ((num / exp) % 10) as usize;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // 10 進数の各桁は 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); // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // 後続のメモリ解放に備えて 0 で初期化する\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < size; i++) {\n        // nums[i] の第 k 位を取得し、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; // d の配列内インデックス j を取得する\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
    /* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfun digit(num: Int, exp: Int): Int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // 10 進数の各桁は 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) // nums[i] の第 k 位を取得し、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 // d の配列内インデックス j を取得する\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
    ### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###\ndef digit(num, exp)\n  # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる\n  (num / exp) % 10\nend\n\n### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###\ndef digit(num, exp)\n  # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる\n  (num / exp) % 10\nend\n\n# ## 計数ソート(nums の k 桁目でソート)###\ndef counting_sort_digit(nums, exp)\n  # 10 進数の各桁は 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) # nums[i] の第 k 位を取得し、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 # d の配列内インデックス j を取得する\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
    コードの可視化

    全画面で見る >

    なぜ最下位桁からソートするのですか?

    連続するソートの各ラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。たとえば、第1ラウンドで \\(a < b\\) となっていても、第2ラウンドで \\(a > b\\) となれば、第2ラウンドの結果が優先されます。数字では高位の優先度が低位より高いため、先に低位をソートし、その後で高位をソートする必要があります。

    ","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\\) とすると、ある1桁に対して計数ソートを実行する時間は \\(O(n + d)\\) であり、全 \\(k\\) 桁をソートする時間は \\(O((n + d)k)\\) です。通常、\\(d\\) と \\(k\\) はどちらも比較的小さいため、時間計算量は \\(O(n)\\) に近づきます。
    • 空間計算量は \\(O(n + d)\\)、非原地ソート:計数ソートと同様に、基数ソートでは長さ \\(n\\) と \\(d\\) の配列 rescounter を補助的に用います。
    • 安定ソート:計数ソートが安定であれば基数ソートも安定です。計数ソートが不安定な場合、基数ソートでは正しいソート結果を保証できません。
    ","path":["第 11 章   ソート","11.10   基数ソート"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   選択ソート","text":"

    選択ソート(selection sort)の仕組みは非常に単純です。ループを開始し、各ラウンドで未ソート区間から最小の要素を選び、整列済み区間の末尾に配置します。

    配列の長さを \\(n\\) とすると、選択ソートの手順は次の図のようになります。

    1. 初期状態では、すべての要素が未ソートであり、未ソートな(インデックス)区間は \\([0, n-1]\\) です。
    2. 区間 \\([0, n-1]\\) 内の最小要素を選び、インデックス \\(0\\) の要素と交換します。これにより、配列の先頭 1 要素が整列済みになります。
    3. 区間 \\([1, n-1]\\) 内の最小要素を選び、インデックス \\(1\\) の要素と交換します。これにより、配列の先頭 2 要素が整列済みになります。
    4. これを繰り返します。\\(n - 1\\) 回の選択と交換を経ると、配列の先頭 \\(n - 1\\) 要素が整列済みになります。
    5. 残った 1 つの要素は必ず最大要素なので、ソートは不要です。これで配列のソートは完了します。
    <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\\) は定数サイズの追加領域しか使用しません。
    • 不安定ソート:次の図のように、要素 nums[i] がそれと等しい要素の右側へ交換され、両者の相対的な順序が変わる可能性があります。

    図 11-3   選択ソートの不安定な例

    ","path":["第 11 章   ソート","11.2   選択ソート"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   ソートアルゴリズム","text":"

    ソートアルゴリズム(sorting algorithm)は、データの集合を特定の順序に従って並べ替えるために用いられます。ソートアルゴリズムは幅広く応用されており、整列済みデータは通常、より効率的に検索、分析、処理できるためです。

    下図に示すように、ソートアルゴリズムにおけるデータ型は整数、浮動小数点数、文字、文字列などです。ソートの判定規則は、数値の大小、文字の ASCII コード順、またはカスタムルールなど、要件に応じて設定できます。

    図 11-1   データ型と判定規則の例

    ","path":["第 11 章   ソート","11.1   ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111","level":2,"title":"11.1.1   評価軸","text":"

    実行効率:ソートアルゴリズムの時間計算量はできるだけ低く、かつ全体の操作回数も少ないこと(時間計算量における定数項が小さいこと)が望まれます。大量データの場合、実行効率はとりわけ重要です。

    インプレース性:その名のとおり、インプレースソートは元の配列を直接操作して並べ替えを行うため、追加の補助配列を必要とせず、メモリを節約できます。通常、インプレースソートはデータの移動操作が少なく、実行速度もより高速です。

    安定性:安定ソートは、並べ替え完了後も、等しい要素の配列内での相対順序が変化しません。

    安定ソートは多段ソートの場面で必要条件となります。学生情報を保存した表があり、第 1 列と第 2 列がそれぞれ氏名と年齢であると仮定します。この場合、不安定ソートによって入力データの順序性が失われる可能性があります。

    # 入力データは氏名順にソートされている\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)\\) に最適化できます。
    • マージソートは分割とマージという 2 つの段階からなり、分割統治戦略を典型的に体現しています。マージソートでは配列を整列する際に補助配列の作成が必要で、空間計算量は \\(O(n)\\) です。一方、連結リストを整列する場合の空間計算量は \\(O(1)\\) まで最適化できます。
    • バケットソートはデータのバケット分配、バケット内ソート、結果の結合という 3 つの手順を含みます。これも分割統治戦略を体現しており、データ量が非常に大きい場合に適しています。バケットソートの鍵は、データを平均的に分配することにあります。
    • カウントソートはバケットソートの特例であり、データの出現回数を数えることで整列を行います。カウントソートはデータ量が大きく、かつデータ範囲が限られている場合に適しており、データを正の整数に変換できることが前提です。
    • 基数ソートは各桁ごとの整列によってデータを整列し、データが固定桁数の数値として表せることを前提とします。
    • 総じて言えば、私たちは高効率で、安定で、インプレースで、さらに適応的であるといった利点を備えたソートアルゴリズムを見つけたいと考えます。しかし、ほかのデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たせるソートアルゴリズムは存在しません。実際の応用では、データの特性に応じて適切なソートアルゴリズムを選ぶ必要があります。
    • 下図では、主流のソートアルゴリズムについて、効率、安定性、インプレース性、適応性などを比較しています。

    図 11-19   ソートアルゴリズムの比較

    ","path":["第 11 章   ソート","11.11   まとめ"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

    Q:ソートアルゴリズムの安定性は、どのような場合に必須ですか?

    現実には、オブジェクトのある属性に基づいて整列することがあります。たとえば、学生には氏名と身長という 2 つの属性があり、多段階のソートを行いたいとします。まず氏名で整列して (A, 180) (B, 185) (C, 170) (D, 170) を得て、その後に身長で整列します。ソートアルゴリズムが不安定である場合、結果は (D, 170) (C, 170) (A, 180) (B, 185) になる可能性があります。

    このように、学生 D と C の位置が入れ替わり、氏名に関する順序性が壊れてしまいます。これは望ましくありません。

    Q:番兵分割において、「右から左へ探索する」順序と「左から右へ探索する」順序は入れ替えられますか?

    できません。最も左端の要素を基準値とする場合は、必ず先に「右から左へ探索する」を行い、その後に「左から右へ探索する」を行う必要があります。この結論はやや直感に反するので、理由を分析してみましょう。

    番兵分割 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] を基準値に選ぶ場合はちょうど逆になり、必ず先に「左から右へ探索する」を行う必要があります。

    Q:クイックソートの再帰深度最適化について、短い配列を選ぶとなぜ再帰深度が \\(\\log n\\) を超えないと保証できるのですか?

    再帰深度とは、現在まだ戻っていない再帰呼び出しの数のことです。各ラウンドの番兵分割では、元の配列を 2 つの部分配列に分けます。再帰深度の最適化後は、下方向に再帰する部分配列の長さは最大でも元の配列長の半分です。最悪の場合でも毎回半分の長さになると仮定すれば、最終的な再帰深度は \\(\\log n\\) になります。

    元のクイックソートを振り返ると、長いほうの配列に対して連続して再帰してしまう可能性があり、最悪の場合は \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) と続き、再帰深度は \\(n\\) になります。再帰深度の最適化により、このような状況を避けられます。

    Q:配列内のすべての要素が等しい場合、クイックソートの時間計算量は \\(O(n^2)\\) になりますか?このような退化はどう処理すべきですか?

    はい。この場合は、番兵分割によって配列を「基準値より小さい」「基準値に等しい」「基準値より大きい」の 3 つの部分に分ける方法を検討できます。下方向に再帰するのは、小さい部分と大きい部分だけです。この方法では、入力要素がすべて等しい配列は、1 回の番兵分割だけで整列を完了できます。

    Q:バケットソートの最悪時間計算量が \\(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 章   スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"章の内容","text":"
    • 5.1   スタック
    • 5.2   キュー
    • 5.3   両端キュー
    • 5.4   まとめ
    ","path":["第 5 章   スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   両端キュー","text":"

    キューでは、先頭要素を削除するか末尾に要素を追加することしかできません。次の図に示すように、両端キュー(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   両端キューの操作効率

    メソッド名 説明 時間計算量 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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20deq.append%282%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E5%B0%BE%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%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E9%A6%96%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%20rear%20%3D%22,%20rear%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_front%20%3D%22,%20pop_front%29%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_rear%20%3D%22,%20pop_rear%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%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":"

    前節を振り返ると、通常の単方向連結リストを使ってキューを実装しました。これは、先頭ノードの削除(デキューに対応)と末尾ノードの後ろへの新規ノード追加(エンキューに対応)を容易に行えるためです。

    両端キューでは、先頭と末尾のどちらでもエンキューとデキューを行えます。言い換えると、両端キューではもう一方の対称方向の操作も実装する必要があります。そのため、両端キューの基盤データ構造として「双方向連結リスト」を採用します。

    次の図に示すように、双方向連結リストの先頭ノードと末尾ノードを両端キューの先頭と末尾と見なし、両端でノードを追加および削除する機能を実現します。

    <1><2><3><4><5>

    図 5-8   連結リストによる両端キューのエンキューとデキュー

    実装コードは次のとおりです:

    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   配列による両端キューのエンキューとデキュー

    キュー実装を土台として、「先頭へのエンキュー」と「末尾からのデキュー」のメソッドを追加するだけで済みます:

    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        # 先頭ポインタを左に 1 つ移動する\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        # 先頭ポインタを 1 つ後ろへ進める\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;        // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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;   // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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   // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを左に 1 つ移動する\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    // 先頭ポインタを 1 つ後ろへ進める\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 // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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; // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを左に 1 つ移動する\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    // 先頭ポインタを右に 1 つ移動する\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,    // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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;       // 先頭ポインタ。先頭要素を指す\n    int queSize;     // 末尾ポインタ。キューの末尾 + 1 を指す\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    // 先頭ポインタを左に 1 つ移動する\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    // 先頭ポインタを 1 つ後ろへ進める\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 // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを左に 1 つ移動する\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        // 先頭ポインタを 1 つ後ろへ進める\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    # 先頭ポインタを左に 1 つ移動する\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    # 先頭ポインタを 1 つ後ろへ進める\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":"

    両端キューはスタックとキューの両方の論理を備えているため、これら 2 つのすべての応用場面を実現でき、さらに高い自由度を提供します。

    私たちが知っているように、ソフトウェアの「元に戻す」機能は通常スタックを使って実装されます。システムは変更操作を毎回スタックに push し、その後 pop によって取り消しを実現します。しかし、システム資源の制約を考慮すると、通常ソフトウェアは取り消し可能な手数を制限します(たとえば \\(50\\) 手まで保存可能)。スタックの長さが \\(50\\) を超えると、ソフトウェアはスタックの底部(先頭)で削除操作を行う必要があります。しかしスタックではこの機能を実現できないため、この場合はスタックの代わりに両端キューを使用する必要があります。なお、「元に戻す」の中核ロジック自体は依然としてスタックの後入れ先出し原則に従っており、両端キューは追加のロジックをより柔軟に実装できるだけです。

    ","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   キュー","text":"

    キュー(queue)は、先入れ先出しの規則に従う線形データ構造です。名前のとおり、キューは順番待ちの現象を模したもので、新しく来た人は絶えずキュー末尾に加わり、キュー先頭にいる人から順に離れていきます。

    下図のように、キューの先頭を「キュー先頭」、末尾を「キュー末尾」と呼びます。要素をキュー末尾に加える操作を「エンキュー」、キュー先頭の要素を削除する操作を「デキュー」と呼びます。

    図 5-4   キューの先入れ先出し規則

    ","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521","level":2,"title":"5.2.1   キューの基本操作","text":"

    キューの基本操作を以下の表に示します。なお、メソッド名はプログラミング言語によって異なる場合があります。ここではスタックと同じ命名を採用します。

    表 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 では、キュークラス Qeque は双方向キューであり、キューとしても使用できる\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 と走査メソッドがないため、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%E5%88%9D%E5%A7%8B%E5%8C%96%E9%98%9F%E5%88%97%0A%20%20%20%20%23%20%E5%9C%A8%20Python%20%E4%B8%AD%EF%BC%8C%E6%88%91%E4%BB%AC%E4%B8%80%E8%88%AC%E5%B0%86%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%B1%BB%20deque%20%E7%9C%8B%E4%BD%9C%E9%98%9F%E5%88%97%E4%BD%BF%E7%94%A8%0A%20%20%20%20%23%20%E8%99%BD%E7%84%B6%20queue.Queue%28%29%20%E6%98%AF%E7%BA%AF%E6%AD%A3%E7%9A%84%E9%98%9F%E5%88%97%E7%B1%BB%EF%BC%8C%E4%BD%86%E4%B8%8D%E5%A4%AA%E5%A5%BD%E7%94%A8%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%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%E9%98%9F%E5%88%97%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%90%8E%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%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":"

    下図のように、連結リストの「先頭ノード」と「末尾ノード」をそれぞれ「キュー先頭」と「キュー末尾」とみなし、キュー末尾ではノードの追加のみ、キュー先頭ではノードの削除のみを行うようにします。

    <1><2><3>

    図 5-5   連結リストでキューを実装したエンキューとデキュー操作

    以下は連結リストでキューを実装するコードです:

    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        # キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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    // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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            // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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    // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\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    # キューが空なら、先頭ノードと末尾ノードの両方をそのノードに向ける\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)\\) であり、そのままではデキュー操作の効率が低くなります。しかし、次の巧妙な方法によってこの問題を回避できます。

    変数 front を用いてキュー先頭要素のインデックスを指し、さらに変数 size でキューの長さを記録できます。rear = front + size と定義すると、この式で得られる rear はキュー末尾要素の次の位置を指します。

    この設計に基づくと、配列内で要素を含む有効区間は [front, rear - 1] となります。各種操作の実装方法を下図に示します。

    • エンキュー操作:入力要素を rear の位置に代入し、size を 1 増やします。
    • デキュー操作:front を 1 増やし、size を 1 減らすだけです。

    このように、エンキューとデキューはいずれも 1 回の操作だけで済み、時間計算量はともに \\(O(1)\\) です。

    <1><2><3>

    図 5-6   配列でキューを実装したエンキューとデキュー操作

    ここで 1 つ問題があります。エンキューとデキューを繰り返すと、frontrear はどちらも右へ移動し続け、配列の末尾に達するとそれ以上進めなくなります。この問題を解決するために、配列を先頭と末尾がつながった「環状配列」とみなします。

    環状配列では、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  # 先頭ポインタ。先頭要素を指す\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        # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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;       // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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;   // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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   // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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 // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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; // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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; // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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,        // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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;       // 先頭ポインタ。先頭要素を指す\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    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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 // 先頭ポインタ。先頭要素を指す\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        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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 # 先頭ポインタ。先頭要素を指す\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    # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\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
    コードの可視化

    全画面で見る >

    上記の実装によるキューにも制約があり、長さを可変にできません。しかし、この問題の解決は難しくなく、配列を動的配列に置き換えれば容量拡張の仕組みを導入できます。興味があれば自分で実装してみてください。

    2 つの実装の比較に関する結論はスタックの場合と同じなので、ここでは繰り返しません。

    ","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)は、後入れ先出しの論理に従う線形データ構造です。

    スタックは机の上に積まれた皿の山にたとえられます。1回に1枚の皿しか動かせないとすると、いちばん下の皿を取り出すには、上にある皿を順番にどかす必要があります。この皿をさまざまな型の要素(整数、文字、オブジェクトなど)に置き換えたものが、スタックというデータ構造です。

    下図のように、積み重なった要素の上端を「スタックトップ」、下端を「スタックボトム」と呼びます。要素をスタックトップに追加する操作を「プッシュ」、スタックトップの要素を削除する操作を「ポップ」と呼びます。

    図 5-1   スタックの後入れ先出しの規則

    ","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511","level":2,"title":"5.1.1   スタックの基本操作","text":"

    スタックの基本操作を以下の表に示します。具体的なメソッド名は使用するプログラミング言語によって異なります。ここでは、一般的な 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%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%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%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%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":"

    スタックの動作の仕組みをより深く理解するために、自分でスタッククラスを実装してみましょう。

    スタックは後入れ先出しの原則に従うため、要素の追加や削除はスタックトップでしか行えません。一方、配列や連結リストでは任意の位置で要素を追加・削除できます。つまり、スタックは制限付きの配列または連結リストとみなせます。 言い換えると、配列や連結リストのうち無関係な操作を「隠蔽」することで、外から見た振る舞いをスタックの特性に合わせられます。

    ","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   連結リストによる実装","text":"

    連結リストでスタックを実装する場合、連結リストの先頭ノードをスタックトップ、末尾ノードをスタックボトムとみなせます。

    下図のように、プッシュ操作では要素を連結リストの先頭に挿入するだけでよく、このノード挿入方法は「頭部挿入法」と呼ばれます。ポップ操作では、先頭ノードを連結リストから削除するだけです。

    <1><2><3>

    図 5-2   連結リストによるスタック実装のプッシュ・ポップ操作

    以下は、連結リストによってスタックを実装したコード例です:

    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":"

    配列でスタックを実装する場合、配列の末尾をスタックトップとして扱えます。下図のように、プッシュとポップはそれぞれ配列末尾への要素追加と削除に対応し、どちらの時間計算量も \\(O(1)\\) です。

    <1><2><3>

    図 5-3   配列によるスタック実装のプッシュ・ポップ操作

    プッシュされる要素は際限なく増える可能性があるため、動的配列を使えば、配列の拡張を自前で処理する必要がありません。以下にコード例を示します:

    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-2","level":2,"title":"5.1.3   2つの実装の比較","text":"

    対応する操作

    どちらの実装も、スタックの定義に含まれる各種操作をサポートします。配列ベースの実装はランダムアクセスも可能ですが、これはスタックの定義範囲を超えているため、通常は利用しません。

    時間効率

    配列ベースの実装では、プッシュとポップの両方があらかじめ確保された連続メモリ上で行われるため、キャッシュ局所性が高く、効率に優れます。ただし、プッシュ時に配列容量を超えると拡張処理が発生し、その1回のプッシュの時間計算量は \\(O(n)\\) になります。

    連結リストベースの実装では、サイズ拡張が非常に柔軟であり、前述のような配列拡張による効率低下はありません。ただし、プッシュ時にはノードオブジェクトの初期化とポインタの更新が必要になるため、効率は相対的に低くなります。もっとも、プッシュする要素自体がノードオブジェクトであれば、初期化の手間を省けるため、効率を高められます。

    以上を踏まえると、プッシュおよびポップの対象が intdouble のような基本データ型である場合、次の結論が得られます。

    • 配列ベースのスタックは拡張時に効率が低下しますが、拡張は低頻度の操作であるため、平均効率はより高くなります。
    • 連結リストベースのスタックは、より安定した効率を提供できます。

    空間効率

    リストを初期化するとき、システムは「初期容量」を割り当てますが、この容量は実際の必要量を上回ることがあります。また、拡張は通常、一定の倍率(たとえば2倍)で行われるため、拡張後の容量も実際の必要量を超える可能性があります。したがって、配列ベースのスタックは一定のメモリ浪費を招く可能性があります。

    一方で、連結リストのノードはポインタを追加で保持する必要があるため、連結リストノードは相対的に大きな領域を占有します。

    以上より、どちらの実装がより省メモリかを単純に断定することはできず、具体的な状況に応じて分析する必要があります。

    ","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   スタックの典型的な応用","text":"
    • ブラウザにおける戻ると進む、ソフトウェアにおける取り消しとやり直し。新しいWebページを開くたびに、ブラウザは直前のページをスタックにプッシュするため、戻る操作によって前のページに戻れます。戻る操作は実際にはポップに相当します。戻ると進むを同時にサポートするには、2つのスタックを組み合わせて実現する必要があります。
    • プログラムのメモリ管理。関数を呼び出すたびに、システムはスタックトップにスタックフレームを追加し、関数のコンテキスト情報を記録します。再帰関数では、下向きに再帰していく段階でプッシュが繰り返され、上向きにバックトラックする段階でポップが繰り返されます。
    ","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":"
    • スタックは後入れ先出しの原則に従うデータ構造であり、配列または連結リストで実装できます。
    • 時間効率の面では、スタックの配列実装は平均効率が高い一方、拡張時には 1 回のプッシュ操作の時間計算量が \\(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:ポップした後、そのノードのメモリを解放する必要はありますか?

    後で取り出したノードを引き続き使うのであれば、メモリを解放する必要はありません。以降そのノードを使わない場合でも、JavaPython などの言語には自動ガベージコレクション機構があるため、手動でメモリを解放する必要はありません。一方、CC++ では手動でメモリを解放する必要があります。

    Q:両端キューは 2 つのスタックをつなげたように見えますが、用途は何ですか?

    両端キューは、スタックとキューの組み合わせ、あるいは 2 つのスタックをつなげたもののような構造です。表しているのはスタック + キューのロジックなので、スタックとキューのすべての応用を実現でき、しかもより柔軟です。

    Q:取り消し(undo)とやり直し(redo)は具体的にどのように実装されますか?

    2 つのスタックを使い、スタック A を取り消し用、スタック B をやり直し用に使います。

    1. ユーザーが操作を 1 つ実行するたびに、その操作をスタック A にプッシュし、スタック B を空にします。
    2. ユーザーが「取り消し」を実行したときは、スタック A から直近の操作をポップし、それをスタック B にプッシュします。
    3. ユーザーが「やり直し」を実行したときは、スタック B から直近の操作をポップし、それをスタック A にプッシュします。
    ","path":["第 5 章   スタックとキュー","5.4   まとめ"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章   木","text":"

    Abstract

    大樹は生命力に満ち、根は深く葉は生い茂り、枝は豊かに広がる。

    それはデータ分割統治の生き生きとした姿を私たちに示してくれる。

    ","path":["第 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 章   木"],"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":"

    まずは簡単な例を考えます。与えられた 1 本の充足二分木について、すべてのノードをレベル順走査の順に配列へ格納すると、各ノードは一意な配列インデックスに対応します。

    レベル順走査の性質に基づくと、親ノードのインデックスと子ノードのインデックスの間にある「対応式」を導けます。あるノードのインデックスが \\(i\\) なら、その左子ノードのインデックスは \\(2i + 1\\) 、右子ノードのインデックスは \\(2i + 2\\) です。以下の図は、各ノードインデックス間の対応関係を示しています。

    図 7-12   充足二分木の配列表現

    対応式は、連結リストにおけるノード参照(ポインタ)と同じ役割を果たします。与えられた配列内の任意のノードについて、この対応式を使えばその左(右)子ノードにアクセスできます。

    ","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732","level":2,"title":"7.3.2   任意の二分木を表現する","text":"

    充足二分木は特殊なケースであり、一般の二分木では中間層に多数の None が存在することがよくあります。レベル順走査の列にはこれらの None が含まれないため、その列だけから None の数や分布位置を推定することはできません。つまり、このレベル順走査列に一致する二分木構造は複数存在し得ます。

    次の図のように、非充足二分木が与えられると、上記の配列表現はすでに成り立ちません。

    図 7-13   レベル順走査列に対応する複数の二分木の可能性

    この問題を解決するために、**レベル順走査列にすべての None を明示的に書き込む**ことを考えられます。次の図のように、このように処理すればレベル順走査列で二分木を一意に表現できます。コード例は以下のとおりです:

    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// int のラッパークラス Integer を使えば、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   完全二分木の配列表現

    以下のコードでは、配列ベースで表現した二分木を実装しており、次の操作を含みます。

    • あるノードが与えられたとき、その値、左(右)子ノード、親ノードを取得する。
    • 前順走査、中順走査、後順走査、レベル順走査の列を取得する。
    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)\\) へ劣化します。

    以下の図に示すように、ノード削除を 2 回行うと、この二分探索木は連結リストへ退化します。

    図 7-24   AVL 木がノード削除後に退化する

    別の例として、以下の図に示す完全二分木に 2 つのノードを挿入すると、木は大きく左に傾き、探索操作の時間計算量もそれに伴って劣化します。

    図 7-25   AVL 木がノード挿入後に退化する

    1962 年、G. M. Adelson-Velsky と E. M. 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 木は二分探索木であると同時に平衡二分木でもあり、これら 2 種類の二分木の性質をすべて満たします。したがって、平衡二分探索木(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\\) です。ここでは、ノードの高さを取得・更新するための 2 つの補助関数を用意します:

    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\\) のノードを「不平衡ノード」と呼びます。ノードの不平衡の形に応じて、回転操作は 4 種類に分かれます。右回転、左回転、右回転してから左回転、左回転してから右回転です。以下でこれらを順に説明します。

    ","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   右回転の手順

    以下の図に示すように、ノード child に右の子ノード(grand_child と記す)がある場合、右回転には 1 ステップ追加する必要があります。すなわち、grand_childnode の左の子ノードにします。

    図 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    # child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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  // child を支点として node を右回転させる\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            // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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    // child を支点として node を右回転させる\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  # child を支点として node を右回転させる\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_1","level":3,"title":"2.   左回転","text":"

    対応する鏡像として、上記の不平衡二分木を左右反転して考えると、以下の図に示す「左回転」が必要になります。

    図 7-28   左回転

    同様に、以下の図に示すように、ノード child に左の子ノード(grand_child と記す)がある場合、左回転にも 1 ステップ追加する必要があります。すなわち、grand_childnode の右の子ノードにします。

    図 7-29   grand_child を持つ左回転

    分かるように、右回転と左回転は論理的に鏡像対称であり、それぞれが解決する 2 種類の不平衡も対称です。この対称性に基づけば、右回転の実装コードにあるすべての leftright に、すべての rightleft に置き換えるだけで、左回転の実装コードが得られます:

    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    # child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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  // child を支点として node を左回転させる\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            // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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    // child を支点として node を左回転させる\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  # child を支点として node を左回転させる\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 では、左回転だけでも右回転だけでも部分木を平衡に戻せません。この場合は、まず child に「左回転」を行い、次に node に「右回転」を行います。

    図 7-30   左回転してから右回転

    ","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4","level":3,"title":"4.   右回転してから左回転","text":"

    以下の図に示すように、上記の不平衡二分木の鏡像のケースでは、まず child に「右回転」を行い、次に node に「左回転」を行います。

    図 7-31   右回転してから左回転

    ","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5","level":3,"title":"5.   回転の選択","text":"

    以下の図に示す 4 種類の不平衡は、上の各ケースにそれぞれ対応しており、必要な操作は順に右回転、左回転してから右回転、右回転してから左回転、左回転です。

    図 7-32   AVL 木の 4 つの回転ケース

    以下の表に示すように、不平衡ノードの平衡係数と、高い側の子ノードの平衡係数の符号を判定することで、その不平衡ノードが上図のどのケースに属するかを判断できます。

    表 7-3   4 種類の回転ケースの選択条件

    不平衡ノードの平衡係数 子ノードの平衡係数 採用すべき回転方法 \\(> 1\\) (左に偏った木) \\(\\geq 0\\) 右回転 \\(> 1\\) (左に偏った木) \\(<0\\) 左回転してから右回転 \\(< -1\\) (右に偏った木) \\(\\leq 0\\) 左回転 \\(< -1\\) (右に偏った木) \\(>0\\) 右回転してから左回転

    使いやすくするために、回転操作を 1 つの関数にカプセル化します。この関数があれば、さまざまな不平衡ケースに対して回転を行い、不平衡ノードを再び平衡に戻せます。コードは次のとおりです:

    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_2","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":"

    以下の図に示すように、二分探索木(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.valnum の大小関係を繰り返し比較します。

    • 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   二分探索木にノードを挿入する

    コード実装では、次の 2 点に注意が必要です。

    • 二分探索木では重複ノードを許可しません。そうでないと定義に反するためです。したがって、挿入対象のノードが木内にすでに存在する場合は、挿入を行わずそのまま返します。
    • ノード挿入を実現するために、ノード 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 の 3 つのケースに分けて対応する削除操作を行います。

    以下の図に示すように、削除対象ノードの次数が \\(0\\) のとき、そのノードは葉ノードであり、直接削除できます。

    図 7-19   二分探索木でノードを削除する(次数 0 )

    以下の図に示すように、削除対象ノードの次数が \\(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 or 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        # tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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    // tmp で cur を上書きする\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 or 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            // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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        // tmp で cur を上書きする\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 or 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    # tmp で cur を上書きする\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":"

    以下の図に示すように、二分木の中順走査は「左 \\(\\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\\) 回のループ内で任意のノードを探索できます。

    しかし、二分探索木でノードの挿入と削除を繰り返すと、二分木が以下の図のような連結リストへ退化する可能性があり、このとき各操作の時間計算量も \\(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

    各ノードは 2 つの参照(ポインタ)を持ち、それぞれ左子ノード(left-child node)と右子ノード(right-child node)を指します。このノードはこれら 2 つの子ノードの親ノード(parent node)と呼ばれます。二分木のあるノードが与えられたとき、そのノードの左子ノードとその配下のノードからなる木をそのノードの左部分木(left subtree)と呼び、同様に右部分木(right subtree)が定義されます。

    二分木では、葉ノードを除くすべてのノードが子ノードと空でない部分木を持ちます。以下の図に示すように、「ノード 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":"

    二分木でよく使われる用語を以下の図に示します。

    • 根ノード(root node):二分木の最上位にあるノードで、親ノードを持ちません。
    • 葉ノード(leaf node):子ノードを持たないノードで、2 本のポインタはいずれも None を指します。
    • 辺(edge):2 つのノードを結ぶ線分、すなわちノード参照(ポインタ)です。
    • ノードが属するレベル(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%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%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%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%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%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%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":"

    連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。以下の図に 1 つの例を示します。

    図 7-3   二分木でノードを挿入・削除する

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
    # ノードの挿入と削除\np = TreeNode(0)\n# n1 -> n2 の間にノード P を挿入する\nn1.left = p\np.left = n2\n# ノード P を削除する\nn1.left = n2\n
    binary_tree.cpp
    /* ノードの挿入と削除 */\nTreeNode* P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1->left = P;\nP->left = n2;\n// ノード P を削除する\nn1->left = n2;\n// メモリを解放する\ndelete P;\n
    binary_tree.java
    TreeNode P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.cs
    /* ノードの挿入と削除 */\nTreeNode P = new(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.go
    /* ノードの挿入と削除 */\n// n1 -> n2 の間にノード P を挿入する\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// ノード P を削除する\nn1.Left = n2\n
    binary_tree.swift
    let P = TreeNode(x: 0)\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P\nP.left = n2\n// ノード P を削除する\nn1.left = n2\n
    binary_tree.js
    /* ノードの挿入と削除 */\nlet P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.ts
    /* ノードの挿入と削除 */\nconst P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.dart
    /* ノードの挿入と削除 */\nTreeNode P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
    binary_tree.rs
    let p = TreeNode::new(0);\n// n1 -> n2 の間にノード P を挿入する\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// n1 -> n2 の間にノード P を挿入する\nn1->left = P;\nP->left = n2;\n// ノード P を削除する\nn1->left = n2;\n// メモリを解放する\nfree(P);\n
    binary_tree.kt
    val P = TreeNode(0)\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P\nP.left = n2\n// ノード P を削除する\nn1.left = n2\n
    binary_tree.rb
    # ノードの挿入と削除\n_p = TreeNode.new(0)\n# n1 -> n2 の間にノード _p を挿入する\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%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%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%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%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%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%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%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%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%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%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%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%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":"

    以下の図に示すように、充足二分木(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":"

    以下の図に示すように、完全二分木(complete binary tree)では最下層のノードだけが完全に埋まっていなくてもよく、しかも最下層のノードは左から右へ連続して詰められていなければなりません。なお、充足二分木も完全二分木の一種です。

    図 7-5   完全二分木

    ","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#3","level":3,"title":"3.   充満二分木","text":"

    以下の図に示すように、充満二分木(full binary tree)では、葉ノードを除くすべてのノードが 2 つの子ノードを持ちます。

    図 7-6   充満二分木

    ","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#4","level":3,"title":"4.   平衡二分木","text":"

    以下の図に示すように、平衡二分木(balanced binary tree)では、任意のノードについて左部分木と右部分木の高さの差の絶対値が 1 を超えません。

    図 7-7   平衡二分木

    ","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#714","level":2,"title":"7.1.4   二分木の退化","text":"

    以下の図は、二分木の理想的な構造と退化した構造を示しています。二分木の各レベルのノードがすべて埋まっていると「充足二分木」となり、すべてのノードが片側に偏ると二分木は「連結リスト」へ退化します。

    • 充足二分木は理想的なケースであり、二分木の「分割統治」の利点を十分に発揮できます。
    • 連結リストはその対極にあり、各種操作はすべて線形操作となり、時間計算量は \\(O(n)\\) まで退化します。

    図 7-8   二分木の最良構造と最悪構造

    以下の表に示すように、最良構造と最悪構造では、二分木の葉ノード数、ノード総数、高さなどが極大または極小になります。

    表 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":"

    次の図に示すように、レベル順走査(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)\\) :すべてのノードを1回ずつ訪問するため、計算量は \\(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)とも呼ばれます。これは「まず行き止まりまで進み、その後で戻って続ける」という走査方法を表しています。

    次の図は、二分木に対して深度優先走査を行う仕組みを示しています。深度優先走査は、二分木全体の外周をぐるりと「一周する」ようなものです。各ノードでは3つの位置に出会い、それぞれが先行順走査・中間順走査・後行順走査に対応します。

    図 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

    深度優先探索は反復によって実装することもできます。興味のある読者は自身で調べてみてください。

    次の図は、二分木の先行順走査における再帰の過程を示しており、「行き」と「帰り」という2つの逆向きの部分に分けられます。

    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)\\) :すべてのノードを1回ずつ訪問するため、計算量は \\(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":"
    • 二分木は非線形データ構造の一種であり、「二分する」分割統治の考え方を体現している。各二分木ノードは 1 つの値と 2 本のポインタを持ち、それぞれ左子ノードと右子ノードを指す。
    • 二分木のあるノードについて、その左(右)子ノードおよびその配下から構成される木を、そのノードの左(右)部分木と呼ぶ。
    • 二分木に関する用語には、根ノード、葉ノード、レベル、次数、辺、高さ、深さなどがある。
    • 二分木の初期化、ノードの挿入、ノードの削除は、連結リストの操作方法と似ている。
    • 一般的な二分木の種類には、perfect 二分木、complete 二分木、full 二分木、平衡二分木がある。perfect 二分木が最も理想的な状態であり、連結リストは退化後の最悪の状態である。
    • 二分木は配列で表現できる。方法としては、ノード値と空き位置をレベル順走査の順に並べ、親ノードと子ノードのインデックス対応関係に基づいてポインタを実現する。
    • 二分木のレベル順走査は幅優先探索の一種であり、「同心円状に外へ広がる」ような逐次的な走査方式を表しており、通常はキューによって実装される。
    • 前順、中順、後順走査はいずれも深さ優先探索に属し、「まず末端まで進み、その後バックトラックして続ける」という走査方式を体現しており、通常は再帰で実装される。
    • 二分探索木は効率的な要素探索データ構造であり、探索、挿入、削除の時間計算量はいずれも \\(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:ノードが 1 つしかない二分木では、木の高さと根ノードの深さはどちらも \\(0\\) ですか?

    はい。高さと深さは通常「通過した辺の本数」として定義されるからです。

    Q:二分木における挿入と削除は通常一連の操作を組み合わせて完了しますが、ここでいう「一連の操作」とは何を指すのでしょうか?リソースの子ノードに対するリソース解放と理解できますか?

    二分探索木を例にすると、ノード削除は 3 つのケースに分けて処理する必要があり、各ケースで複数段階のノード操作が必要になります。

    Q:なぜ DFS による二分木走査には前順・中順・後順の 3 種類があり、それぞれどのような用途があるのですか?

    配列の順方向走査と逆方向走査に似て、前順・中順・後順走査は二分木の 3 つの走査方法であり、特定の順序で走査結果を得るために使えます。たとえば二分探索木では、ノードの大小関係が 左子ノードの値 < 根ノードの値 < 右子ノードの値 を満たすため、「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」の優先順で木を走査すれば、整列済みのノード列を得られます。

    Q:右回転操作は不平衡ノード nodechildgrand_child の関係を処理するものですが、node の親ノードと node の元の接続は維持しなくてよいのですか?右回転後に切れてしまいませんか?

    この問題は再帰の視点から考える必要があります。右回転操作 right_rotate(root) に渡されるのは部分木の根ノードであり、最終的に return child によって回転後の部分木の根ノードを返します。部分木の根ノードとその親ノードの接続は、この関数の返却後に行われるため、右回転操作自身が管理する範囲には含まれません。

    Q:C++ では関数を privatepublic に分けますが、この設計にはどのような考えがありますか?なぜ height() 関数と updateHeight() 関数をそれぞれ publicprivate に置くのですか?

    主に、そのメソッドの利用範囲を見て決めます。メソッドがクラス内部でしか使われないなら、private に設計します。たとえば、利用者が updateHeight() を単独で呼び出しても意味はなく、これは挿入や削除の途中の 1 ステップにすぎません。一方で height() はノードの高さにアクセスするためのもので、vector.size() に似た役割を持つため、使いやすいように public に設定します。

    Q:入力データの集合から二分探索木をどのように構築しますか?根ノードの選び方は重要ですか?

    はい。木の構築方法は、二分探索木のコード中の build_tree() メソッドですでに示されています。根ノードの選択については、通常は入力データをソートし、その中央の要素を根ノードにしてから、左右の部分木を再帰的に構築します。こうすることで、木の平衡性を最大限に保てます。

    Q:Java では、文字列比較には必ず equals() メソッドを使うべきですか?

    Java では、基本データ型については == を使って 2 つの変数の値が等しいかどうかを比較します。参照型については、この 2 つの記法の働き方は異なります。

    • == :2 つの変数が同じオブジェクトを指しているか、つまりメモリ上の位置が同じかどうかを比較するために使います。
    • equals():2 つのオブジェクトの値が等しいかどうかを比較するために使います。

    したがって、値を比較したい場合は equals() を使うべきです。ただし、String a = \"hi\"; String b = \"hi\"; によって初期化された文字列は文字列定数プールに格納され、同じオブジェクトを指すため、a == b でも 2 つの文字列の内容を比較できます。

    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/ja/stylesheets/animation_player.css b/ja/stylesheets/animation_player.css index 7fe5f14f7..025150f72 100644 --- a/ja/stylesheets/animation_player.css +++ b/ja/stylesheets/animation_player.css @@ -176,4 +176,4 @@ font-size: 0.7rem; } } -/*! update cache: 20260410024950 */ +/*! update cache: 20260410223953 */ diff --git a/ja/stylesheets/extra.css b/ja/stylesheets/extra.css index f518554e0..692d6d3e9 100644 --- a/ja/stylesheets/extra.css +++ b/ja/stylesheets/extra.css @@ -790,4 +790,4 @@ a:hover .device-on-hover { flex: 1 1 30%; } } -/*! update cache: 20260410024950 */ +/*! update cache: 20260410223953 */ diff --git a/ja/stylesheets/giscus-dark.css b/ja/stylesheets/giscus-dark.css index 2a2f9f465..012db16ea 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: 20260410024950 */ +/*! update cache: 20260410223953 */ diff --git a/ja/stylesheets/giscus-light.css b/ja/stylesheets/giscus-light.css index 42415e974..aa8f316d6 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: 20260410024950 */ +/*! update cache: 20260410223953 */ diff --git a/javascripts/animation_player.js b/javascripts/animation_player.js index 917a4a196..967da8aa4 100644 --- a/javascripts/animation_player.js +++ b/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410024918 */ +/*! update cache: 20260410223920 */ diff --git a/javascripts/katex.js b/javascripts/katex.js index 167ceeaaa..b3c4656aa 100644 --- a/javascripts/katex.js +++ b/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410024918 */ +/*! update cache: 20260410223920 */ diff --git a/javascripts/mathjax.js b/javascripts/mathjax.js index bd1b60e32..8801a49ef 100644 --- a/javascripts/mathjax.js +++ b/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260410024918 */ +/*! update cache: 20260410223920 */ diff --git a/javascripts/starfield.js b/javascripts/starfield.js index 31fc1a8bf..8393297e9 100644 --- a/javascripts/starfield.js +++ b/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410024918 */ +/*! update cache: 20260410223920 */ diff --git a/ru/assets/javascripts/bundle.c2b142ea.min.js b/ru/assets/javascripts/bundle.c2b142ea.min.js index 1da23f141..8805ebc1e 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: 20260410025001 */ +/*! update cache: 20260410224004 */ diff --git a/ru/chapter_data_structure/character_encoding/index.html b/ru/chapter_data_structure/character_encoding/index.html index 3e1a2755c..534707708 100644 --- a/ru/chapter_data_structure/character_encoding/index.html +++ b/ru/chapter_data_structure/character_encoding/index.html @@ -4437,9 +4437,8 @@

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

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

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

    -

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

    -

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

    -

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

    +

    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

    diff --git a/ru/javascripts/animation_player.js b/ru/javascripts/animation_player.js index d44ae7421..0a6faff41 100644 --- a/ru/javascripts/animation_player.js +++ b/ru/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410025001 */ +/*! update cache: 20260410224004 */ diff --git a/ru/javascripts/katex.js b/ru/javascripts/katex.js index ba6992eed..73178e6c8 100644 --- a/ru/javascripts/katex.js +++ b/ru/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410025001 */ +/*! update cache: 20260410224004 */ diff --git a/ru/javascripts/mathjax.js b/ru/javascripts/mathjax.js index 84dc43b0a..42717a857 100644 --- a/ru/javascripts/mathjax.js +++ b/ru/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260410025001 */ +/*! update cache: 20260410224004 */ diff --git a/ru/javascripts/starfield.js b/ru/javascripts/starfield.js index 64bdb52f6..ccd54b734 100644 --- a/ru/javascripts/starfield.js +++ b/ru/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410025001 */ +/*! update cache: 20260410224004 */ diff --git a/ru/search.json b/ru/search.json index 4545e19b0..ff1484614 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 часто используемые символы занимают 2 байта, а некоторые редкие символы - 3 байта и даже 4 байта.

    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, можно заметить, что индекс первого элемента массива равен \\(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 diff --git a/ru/stylesheets/animation_player.css b/ru/stylesheets/animation_player.css index 8efd28dda..ec903e464 100644 --- a/ru/stylesheets/animation_player.css +++ b/ru/stylesheets/animation_player.css @@ -176,4 +176,4 @@ font-size: 0.7rem; } } -/*! update cache: 20260410025001 */ +/*! update cache: 20260410224004 */ diff --git a/ru/stylesheets/extra.css b/ru/stylesheets/extra.css index fcca331aa..3fc18b903 100644 --- a/ru/stylesheets/extra.css +++ b/ru/stylesheets/extra.css @@ -790,4 +790,4 @@ a:hover .device-on-hover { flex: 1 1 30%; } } -/*! update cache: 20260410025001 */ +/*! update cache: 20260410224004 */ diff --git a/ru/stylesheets/giscus-dark.css b/ru/stylesheets/giscus-dark.css index 61840f1d3..3da8d654c 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: 20260410025001 */ +/*! update cache: 20260410224004 */ diff --git a/ru/stylesheets/giscus-light.css b/ru/stylesheets/giscus-light.css index 52b780252..a1bfc53b5 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: 20260410025001 */ +/*! update cache: 20260410224004 */ diff --git a/search.json b/search.json index dd1acc67c..c37082087 100644 --- a/search.json +++ b/search.json @@ -1 +1 @@ -{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"第 16 章   附录","text":"","path":["第 16 章   附录"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"本章内容","text":"
    • 16.1   编程环境安装
    • 16.2   一起参与创作
    • 16.3   术语表
    ","path":["第 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. 点击“编辑图标”,如果遇到“需要 Fork 此仓库”的提示,请同意该操作。
    2. 修改 Markdown 源文件内容,检查内容的正确性,并尽量保持排版格式的统一。
    3. 在页面底部填写修改说明,然后点击“Propose file change”按钮。页面跳转后,点击“Create 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. 将本地所做更改 Commit ,然后 Push 至远程仓库。
    5. 刷新仓库网页,点击“Create 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":"

    推荐使用开源、轻量的 VS Code 作为本地集成开发环境(IDE)。访问 VS Code 官网,根据操作系统选择相应版本的 VS Code 进行下载和安装。

    图 16-1   从官网下载 VS Code

    VS Code 拥有强大的扩展包生态系统,支持大多数编程语言的运行和调试。以 Python 为例,安装“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 大 \\(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 AVL 树 red-black tree 红黑树 level-order traversal 层序遍历 breadth-first traversal 广度优先遍历 depth-first traversal 深度优先遍历 binary search tree 二叉搜索树 balanced binary search tree 平衡二叉搜索树 balance factor 平衡因子 heap 堆 max heap 大顶堆 min heap 小顶堆 priority queue 优先队列 heapify 堆化 top-\\(k\\) problem Top-\\(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 章   数组与链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"本章内容","text":"
    • 4.1   数组
    • 4.2   链表
    • 4.3   列表
    • 4.4   内存与缓存 *
    • 4.5   小结
    ","path":["第 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":"

    我们可以根据需求选用数组的两种初始化方式:无初始值、给定初始值。在未指定初始值的情况下,大多数编程语言会将数组元素初始化为 \\(0\\) :

    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
    可视化运行

    全屏观看 >

    ","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    \"\"\"在数组的索引 index 处插入元素 num\"\"\"\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 _num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    ### 在数组的索引 index 处插入元素 num ###\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// 请注意,JavaScript 的 Array 是动态数组,可以直接扩展\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// 请注意,TypeScript 的 Array 是动态数组,可以直接扩展\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# 请注意,Ruby 的 Array 是动态数组,可以直接扩展\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 中分别被记为 nullnullptrNone
    • 在 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
    可视化运行

    全屏观看 >

    数组整体是一个变量,比如数组 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 所示,假设我们想在相邻的两个节点 n0n1 之间插入一个新节点 P ,则只需改变两个节点引用(指针)即可,时间复杂度为 \\(O(1)\\) 。

    相比之下,在数组中插入元素的时间复杂度为 \\(O(n)\\) ,在大数据量下的效率较低。

    图 4-6   链表插入节点示例

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def insert(n0: ListNode, P: ListNode):\n    \"\"\"在链表的节点 n0 之后插入节点 P\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
    linked_list.cpp
    /* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.java
    /* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.cs
    /* 在链表的节点 n0 之后插入节点 P */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.go
    /* 在链表的节点 n0 之后插入节点 P */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
    linked_list.swift
    /* 在链表的节点 n0 之后插入节点 P */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
    linked_list.js
    /* 在链表的节点 n0 之后插入节点 P */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.ts
    /* 在链表的节点 n0 之后插入节点 P */\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
    /* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
    linked_list.rs
    /* 在链表的节点 n0 之后插入节点 P */\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
    /* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.kt
    /* 在链表的节点 n0 之后插入节点 P */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
    linked_list.rb
    ### 在链表的节点 n0 之后插入节点 _p ###\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-vs","level":2,"title":"4.2.2   数组 vs. 链表","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":"

    单向链表通常用于实现栈、队列、哈希表和图等数据结构。

    • 栈与队列:当插入和删除操作都在链表的一端进行时,它表现的特性为先进后出,对应栈;当插入操作在链表的一端进行,删除操作在链表的另一端进行,它表现的特性为先进先出,对应队列。
    • 哈希表:链式地址是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表中。
    • 图:邻接表是表示图的一种常用方式,其中图的每个顶点都与一个链表相关联,链表中的每个元素都代表与该顶点相连的其他顶点。

    双向链表常用于需要快速查找前一个和后一个元素的场景。

    • 高级数据结构:比如在红黑树、B 树中,我们需要访问节点的父节点,这可以通过在节点中保存一个指向父节点的引用来实现,类似于双向链表。
    • 浏览器历史:在网页浏览器中,当用户点击前进或后退按钮时,浏览器需要知道用户访问过的前一个和后一个网页。双向链表的特性使得这种操作变得简单。
    • LRU 算法:在缓存淘汰(LRU)算法中,我们需要快速找到最近最少使用的数据,以及支持快速添加和删除节点。这时候使用双向链表就非常合适。

    环形链表常用于需要周期性操作的场景,比如操作系统的资源调度。

    • 时间片轮转调度算法:在操作系统中,时间片轮转调度算法是一种常见的 CPU 调度算法,它需要对一组进程进行循环。每个进程被赋予一个时间片,当时间片用完时,CPU 将切换到下一个进程。这种循环操作可以通过环形链表来实现。
    • 数据缓冲区:在某些数据缓冲区的实现中,也可能会使用环形链表。比如在音频、视频播放器中,数据流可能会被分成多个缓冲块并放入一个环形链表,以便实现无缝播放。
    ","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   列表","text":"

    列表(list)是一个抽象的数据结构概念,它表示元素的有序集合,支持元素访问、修改、添加、删除和遍历等操作,无须使用者考虑容量限制的问题。列表可以基于链表或数组实现。

    • 链表天然可以看作一个列表,其支持元素增删查改操作,并且可以灵活动态扩容。
    • 数组也支持元素增删查改,但由于其长度不可变,因此只能看作一个具有长度限制的列表。

    当使用数组实现列表时,长度不可变的性质会导致列表的实用性降低。这是因为我们通常无法事先确定需要存储多少数据,从而难以选择合适的列表长度。若长度过小,则很可能无法满足使用需求;若长度过大,则会造成内存空间浪费。

    为解决此问题,我们可以使用动态数组(dynamic array)来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。

    实际上,许多编程语言中的标准库提供的列表是基于动态数组实现的,例如 Python 中的 list 、Java 中的 ArrayList 、C++ 中的 vector 和 C# 中的 List 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。

    ","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// 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[])\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
    可视化运行

    全屏观看 >

    ","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
    可视化运行

    全屏观看 >

    ","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)  # 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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:]...)...) // 在索引 3 处插入数字 6\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) // 在索引 3 处插入数字 6\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); // 在索引 3 处插入数字 6\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); // 在索引 3 处插入数字 6\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); // 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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) # 在索引 3 处插入数字 6\n\n# 删除元素\nnums.delete_at(3) # 删除索引 3 处的元素\n
    可视化运行

    全屏观看 >

    ","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
    可视化运行

    全屏观看 >

    ","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
    可视化运行

    全屏观看 >

    ","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
    可视化运行

    全屏观看 >

    ","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 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。

    图 4-10   硬盘、内存和缓存之间的数据流通

    ","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:为什么数组要求相同类型的元素,而在链表中却没有强调相同类型呢?

    链表由节点组成,节点之间通过引用(指针)连接,各个节点可以存储不同类型的数据,例如 intdoublestringobject 等。

    相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,数组同时包含 intlong 两种类型,单个元素分别占用 4 字节和 8 字节 ,此时就不能用以下公式计算偏移量了,因为数组中包含了两种“元素长度”。

    # 元素内存地址 = 数组内存地址(首元素内存地址) + 元素长度 * 元素索引\n

    Q:删除节点 P 后,是否需要把 P.next 设为 None 呢?

    不修改 P.next 也可以。从该链表的角度看,从头节点遍历到尾节点已经不会遇到 P 了。这意味着节点 P 已经从链表中删除了,此时节点 P 指向哪里都不会对该链表产生影响。

    从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。

    Q:在链表中插入和删除操作的时间复杂度是 \\(O(1)\\) 。但是增删之前都需要 \\(O(n)\\) 的时间查找元素,那为什么时间复杂度不是 \\(O(n)\\) 呢?

    如果是先查找元素、再删除元素,时间复杂度确实是 \\(O(n)\\) 。然而,链表的 \\(O(1)\\) 增删的优势可以在其他应用上得到体现。例如,双向队列适合使用链表实现,我们维护一个指针变量始终指向头节点、尾节点,每次插入与删除操作都是 \\(O(1)\\) 。

    Q:图“链表定义与存储方式”中,浅蓝色的存储节点指针是占用一块内存地址吗?还是和节点值各占一半呢?

    该示意图只是定性表示,定量表示需要根据具体情况进行分析。

    • 不同类型的节点值占用的空间是不同的,比如 intlongdouble 和实例对象等。
    • 指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。

    Q:在列表末尾添加元素是否时时刻刻都为 \\(O(1)\\) ?

    如果添加元素时超出列表长度,则需要先扩容列表再添加。系统会申请一块新的内存,并将原列表的所有元素搬运过去,这时候时间复杂度就会是 \\(O(n)\\) 。

    Q:“列表的出现极大地提高了数组的实用性,但可能导致部分内存空间浪费”,这里的空间浪费是指额外增加的变量如容量、长度、扩容倍数所占的内存吗?

    这里的空间浪费主要有两方面含义:一方面,列表都会设定一个初始长度,我们不一定需要用这么多;另一方面,为了防止频繁扩容,扩容一般会乘以一个系数,比如 \\(\\times 1.5\\) 。这样一来,也会出现很多空位,我们通常不能完全填满它们。

    Q:在 Python 中初始化 n = [1, 2, 3] 后,这 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 的性能会更好。

    另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 stackqueue ,而非链表。

    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 章   回溯"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"本章内容","text":"
    • 13.1   回溯算法
    • 13.2   全排列问题
    • 13.3   子集和问题
    • 13.4   N 皇后问题
    • 13.5   小结
    ","path":["第 13 章   回溯"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   回溯算法","text":"

    回溯算法(backtracking algorithm)是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。

    回溯算法通常采用“深度优先搜索”来遍历解空间。在“二叉树”章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来,我们利用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。

    例题一

    给定一棵二叉树,搜索并记录所有值为 \\(7\\) 的节点,请返回节点列表。

    对于此题,我们前序遍历这棵树,并判断当前节点的值是否为 \\(7\\) ,若是,则将该节点的值加入结果列表 res 之中。相关过程实现如图 13-1 和以下代码所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序遍历:例题一\"\"\"\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    ### 前序遍历:例题一 ###\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":"

    之所以称之为回溯算法,是因为该算法在搜索解空间时会采用“尝试”与“回退”的策略。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。

    对于例题一,访问每个节点都代表一次“尝试”,而越过叶节点或返回父节点的 return 则表示“回退”。

    值得说明的是,回退并不仅仅包括函数返回。为解释这一点,我们对例题一稍作拓展。

    例题二

    在二叉树中搜索所有值为 \\(7\\) 的节点,请返回根节点到这些节点的路径。

    在例题一代码的基础上,我们需要借助一个列表 path 记录访问过的节点路径。当访问到值为 \\(7\\) 的节点时,则复制 path 并添加进结果列表 res 。遍历完成后,res 中保存的就是所有的解。代码如下所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序遍历:例题二\"\"\"\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    ### 前序遍历:例题二 ###\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":"

    复杂的回溯问题通常包含一个或多个约束条件,约束条件通常可用于“剪枝”。

    例题三

    在二叉树中搜索所有值为 \\(7\\) 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 \\(3\\) 的节点。

    为了满足以上约束条件,我们需要添加剪枝操作:在搜索过程中,若遇到值为 \\(3\\) 的节点,则提前返回,不再继续搜索。代码如下所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序遍历:例题三\"\"\"\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    ### 前序遍历:例题三 ###\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

    接下来,我们基于框架代码来解决例题三。状态 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    \"\"\"回溯算法:例题三\"\"\"\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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### 回溯算法:例题三 ###\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 的搜索过程对比

    相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰唆,但通用性更好。实际上,许多回溯问题可以在该框架下解决。我们只需根据具体问题来定义 statechoices ,并实现框架中的各个方法即可。

    ","path":["第 13 章   回溯","13.1   回溯算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   常用术语","text":"

    为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例,如表 13-1 所示。

    表 13-1   常见的回溯算法术语

    名词 定义 例题三 解(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 \\times n\\) 的棋盘上放置 \\(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":"

    为了满足列约束,我们可以利用一个长度为 \\(n\\) 的布尔型数组 cols 记录每一列是否有皇后。在每次决定放置前,我们通过 cols 将已有皇后的列进行剪枝,并在回溯中动态更新 cols 的状态。

    Tip

    请注意,矩阵的起点位于左上角,其中行索引从上到下增加,列索引从左到右增加。

    那么,如何处理对角线约束呢?设棋盘中某个格子的行列索引为 \\((row, col)\\) ,选定矩阵中的某条主对角线,我们发现该对角线上所有格子的行索引减列索引都相等,即主对角线上所有格子的 \\(row - col\\) 为恒定值。

    也就是说,如果两个格子满足 \\(row_1 - col_1 = row_2 - col_2\\) ,则它们一定处在同一条主对角线上。利用该规律,我们可以借助图 13-18 所示的数组 diags1 记录每条主对角线上是否有皇后。

    同理,次对角线上的所有格子的 \\(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\\) ,即数组 diags1diags2 的长度都为 \\(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)\\) 空间,数组 colsdiags1diags2 皆使用 \\(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\\) ,我们将第二个 \\(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":"

    请注意,虽然 selectedduplicated 都用于剪枝,但两者的目标不同。

    • 重复选择剪枝:整个搜索过程中只有一个 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; // 初始化解的数量为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 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    # 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 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  // 剪枝二:从 start 开始遍历,避免生成重复子集\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超过 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  # 剪枝二:从 start 开始遍历,避免生成重复子集\n  for i in start...choices.length\n    # 剪枝一:若子集和超过 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    # 剪枝二:从 start 开始遍历,避免生成重复子集\n    # 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超过 target ,则直接结束循环\n        # 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0:\n            break\n        # 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0 {\n            break\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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  // 剪枝二:从 start 开始遍历,避免生成重复子集\n  // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超过 target ,则直接结束循环\n    // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超过 target ,则直接跳过\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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  # 剪枝二:从 start 开始遍历,避免生成重复子集\n  # 剪枝三:从 start 开始遍历,避免重复选择同一元素\n  for i in start...choices.length\n    # 剪枝一:若子集和超过 target ,则直接结束循环\n    # 这是因为数组已排序,后边元素更大,子集和一定超过 target\n    break if target - choices[i] < 0\n    # 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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-q-a","level":3,"title":"2.   Q & A","text":"

    Q:怎么理解回溯和递归的关系?

    总的来看,回溯是一种“算法策略”,而递归更像是一个“工具”。

    • 回溯算法通常基于递归实现。然而,回溯是递归的应用场景之一,是递归在搜索问题中的应用。
    • 递归的结构体现了“子问题分解”的解题范式,常用于解决分治、回溯、动态规划(记忆化递归)等问题。
    ","path":["第 13 章   回溯","13.5   小结"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章   复杂度分析","text":"

    Abstract

    复杂度分析犹如浩瀚的算法宇宙中的时空向导。

    它带领我们在时间与空间这两个维度上深入探索,寻找更优雅的解决方案。

    ","path":["第 2 章   复杂度分析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"本章内容","text":"
    • 2.1   算法效率评估
    • 2.2   迭代与递归
    • 2.3   时间复杂度
    • 2.4   空间复杂度
    • 2.5   小结
    ","path":["第 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 循环是最常见的迭代形式之一,适合在预先知道迭代次数时使用。

    以下函数基于 for 循环实现了求和 \\(1 + 2 + \\dots + n\\) ,求和结果使用变量 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 循环中,程序每轮都会先检查条件,如果条件为真,则继续执行,否则就结束循环。

    下面我们用 while 循环来实现求和 \\(1 + 2 + \\dots + n\\) :

    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_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 所示,这样不断递归调用下去,最终将产生一棵层数为 \\(n\\) 的递归树(recursion tree)。

    图 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() 在循环中调用了 \\(n\\) 次 function() ,每轮中的 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_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_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 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":"

    算法效率评估

    • 时间效率和空间效率是衡量算法优劣的两个主要评价指标。
    • 我们可以通过实际测试来评估算法效率,但难以消除测试环境的影响,且会耗费大量计算资源。
    • 复杂度分析可以消除实际测试的弊端,分析结果适用于所有运行平台,并且能够揭示算法在不同数据规模下的效率。

    时间复杂度

    • 时间复杂度用于衡量算法运行时间随数据量增长的趋势,可以有效评估算法效率,但在某些情况下可能失效,如在输入的数据量较小或时间复杂度相同时,无法精确对比算法效率的优劣。
    • 最差时间复杂度使用大 \\(O\\) 符号表示,对应函数渐近上界,反映当 \\(n\\) 趋向正无穷时,操作数量 \\(T(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)与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。

    下面以几种常见的编程语言为例来说明。

    • C 语言是过程式编程语言,没有面向对象的概念,所以只有函数。但我们可以通过创建结构体(struct)来模拟面向对象编程,与结构体相关联的函数就相当于其他编程语言中的方法。
    • Java 和 C# 是面向对象的编程语言,代码块(方法)通常作为某个类的一部分。静态方法的行为类似于函数,因为它被绑定在类上,不能访问特定的实例变量。
    • C++ 和 Python 既支持过程式编程(函数),也支持面向对象编程(方法)。

    Q:图解“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?

    不是,该图展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。

    假设取 \\(n = 8\\) ,你可能会发现每条曲线的值与函数对应不上。这是因为每条曲线都包含一个常数项,用于将取值范围压缩到一个视觉舒适的范围内。

    在实际中,因为我们通常不知道每个方法的“常数项”复杂度是多少,所以一般无法仅凭复杂度来选择 \\(n = 8\\) 之下的最优解法。但对于 \\(n = 8^5\\) 就很好选了,这时增长趋势已经占主导了。

    Q 是否存在根据实际使用场景,选择牺牲时间(或空间)来设计算法的情况?

    在实际应用中,大部分情况会选择牺牲空间换时间。例如数据库索引,我们通常选择建立 B+ 树或哈希索引,占用大量内存空间,以换取 \\(O(\\log n)\\) 甚至 \\(O(1)\\) 的高效查询。

    在空间资源宝贵的场景,也会选择牺牲时间换空间。例如在嵌入式开发中,设备内存很宝贵,工程师可能会放弃使用哈希表,选择使用数组顺序查找,以节省内存占用,代价是查找变慢。

    ","path":["第 2 章   复杂度分析","2.5   小结"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   时间复杂度","text":"

    运行时间可以直观且准确地反映算法的效率。如果我们想准确预估一段代码的运行时间,应该如何操作呢?

    1. 确定运行平台,包括硬件配置、编程语言、系统环境等,这些因素都会影响代码的运行效率。
    2. 评估各种计算操作所需的运行时间,例如加法操作 + 需要 1 ns ,乘法操作 * 需要 10 ns ,打印操作 print() 需要 5 ns 等。
    3. 统计代码中所有的计算操作,并将所有操作的执行时间求和,从而得到运行时间。

    例如在以下代码中,输入数据大小为 \\(n\\) :

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # 在某运行平台下\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # 循环 n 次\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
    // 在某运行平台下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
    // 在某运行平台下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
    // 在某运行平台下\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
    // 在某运行平台下\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // 循环 n 次\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
    // 在某运行平台下\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 循环 n 次\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
    // 在某运行平台下\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 循环 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // 在某运行平台下\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 循环 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // 在某运行平台下\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // 循环 n 次\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
    // 在某运行平台下\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // 循环 n 次\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
    // 在某运行平台下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
    // 在某运行平台下\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 循环 n 次\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
    # 在某运行平台下\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # 循环 n 次\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

    根据以上方法,可以得到算法的运行时间为 \\((6n + 12)\\) ns :

    \\[ 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\\) ,给定三个算法 ABC

    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 只有 \\(1\\) 个打印操作,算法运行时间不随着 \\(n\\) 增大而增长。我们称此算法的时间复杂度为“常数阶”。
    • 算法 B 中的打印操作需要循环 \\(n\\) 次,算法运行时间随着 \\(n\\) 增大呈线性增长。此算法的时间复杂度被称为“线性阶”。
    • 算法 C 中的打印操作需要循环 \\(1000000\\) 次,虽然运行时间很长,但它与输入数据大小 \\(n\\) 无关。因此 C 的时间复杂度和 A 相同,仍为“常数阶”。

    图 2-7   算法 A、B 和 C 的时间增长趋势

    相较于直接统计算法的运行时间,时间复杂度分析有哪些特点呢?

    • 时间复杂度能够有效评估算法效率。例如,算法 B 的运行时间呈线性增长,在 \\(n > 1\\) 时比算法 A 更慢,在 \\(n > 1000000\\) 时比算法 C 更慢。事实上,只要输入数据大小 \\(n\\) 足够大,复杂度为“常数阶”的算法一定优于“线性阶”的算法,这正是时间增长趋势的含义。
    • 时间复杂度的推算方法更简便。显然,运行平台和计算操作类型都与算法运行时间的增长趋势无关。因此在时间复杂度分析中,我们可以简单地将所有计算操作的执行时间视为相同的“单位时间”,从而将“计算操作运行时间统计”简化为“计算操作数量统计”,这样一来估算难度就大大降低了。
    • 时间复杂度也存在一定的局限性。例如,尽管算法 AC 的时间复杂度相同,但实际运行时间差别很大。同样,尽管算法 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)\\) ,这个数学符号称为大 \\(O\\) 记号(big-\\(O\\) notation),表示函数 \\(T(n)\\) 的渐近上界(asymptotic upper bound)。

    时间复杂度分析本质上是计算“操作数量 \\(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","level":3,"title":"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","level":3,"title":"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 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\\) 为输入数据大小;在第二个示例中,数组长度 \\(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 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":"

    生物学的“细胞分裂”是指数阶增长的典型例子:初始状态为 \\(1\\) 个细胞,分裂一轮后变为 \\(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 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 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 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 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    # 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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  // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 n 个\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
    time_complexity.rb
    ### 阶乘阶(递归实现)###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # 从 1 个分裂出 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":"

    算法的时间效率往往不是固定的,而是与输入数据的分布有关。假设输入一个长度为 \\(n\\) 的数组 nums ,其中 nums 由从 \\(1\\) 至 \\(n\\) 的数字组成,每个数字只出现一次;但元素顺序是随机打乱的,任务目标是返回元素 \\(1\\) 的索引。我们可以得出以下结论。

    • nums = [?, ?, ..., 1] ,即当末尾元素是 \\(1\\) 时,需要完整遍历数组,达到最差时间复杂度 \\(O(n)\\) 。
    • nums = [1, ?, ?, ...] ,即当首个元素为 \\(1\\) 时,无论数组多长都不需要继续遍历,达到最佳时间复杂度 \\(\\Omega(1)\\) 。

    “最差时间复杂度”对应函数渐近上界,使用大 \\(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    \"\"\"查找数组 nums 中数字 1 所在索引\"\"\"\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    // 使用系统时间生成随机种子\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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### 查找数组 nums 中数字 1 所在索引 ###\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 章   数据结构"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"本章内容","text":"
    • 3.1   数据结构分类
    • 3.2   基本数据类型
    • 3.3   数字编码 *
    • 3.4   字符编码 *
    • 3.5   小结
    ","path":["第 3 章   数据结构"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   基本数据类型","text":"

    当谈及计算机中的数据时,我们会想到文本、图片、视频、语音、3D 模型等各种形式。尽管这些数据的组织形式各异,但它们都由各种基本数据类型构成。

    基本数据类型是 CPU 可以直接进行运算的类型,在算法中直接被使用,主要包括以下几种。

    • 整数类型 byteshortintlong
    • 浮点数类型 floatdouble ,用于表示小数。
    • 字符类型 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 类型,单个字符实际上是长度为 1 的字符串 str
    • C 和 C++ 未明确规定基本数据类型的大小,而因实现和平台各异。表 3-1 遵循 LP64 数据模型,其用于包括 Linux 和 macOS 在内的 Unix 64 位操作系统。
    • 字符 char 的大小在 C 和 C++ 中为 1 字节,在大多数编程语言中取决于特定的字符编码方法,详见“字符编码”章节。
    • 即使表示布尔量仅需 1 位(\\(0\\) 或 \\(1\\)),它在内存中通常也存储为 1 字节。这是因为现代计算机 CPU 通常将 1 字节作为最小寻址内存单元。

    那么,基本数据类型与数据结构之间有什么联系呢?我们知道,数据结构是在计算机中组织与存储数据的方式。这句话的主语是“结构”而非“数据”。

    如果想表示“一排数字”,我们自然会想到使用数组。这是因为数组的线性结构可以表示数字的相邻关系和顺序关系,但至于存储的内容是整数 int、小数 float 还是字符 char ,则与“数据结构”无关。

    换句话说,基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”。例如以下代码,我们用相同的数据结构(数组)来存储与表示不同的基本数据类型,包括 intfloatcharbool 等。

    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
    可视化运行

    全屏观看 >

    ","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 字符集。它在 ASCII 的 7 位基础上扩展到 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 的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。

    自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截至 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号甚至表情符号等。在庞大的 Unicode 字符集中,常用的字符占用 2 字节,有些生僻的字符占用 3 字节甚至 4 字节。

    Unicode 是一种通用字符集,本质上是给每个字符分配一个编号(称为“码点”),但它并没有规定在计算机中如何存储这些字符码点。我们不禁会问:当多种长度的 Unicode 码点同时出现在一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符?

    对于以上问题,一种直接的解决方案是将所有字符存储为等长的编码。如图 3-7 所示,“Hello”中的每个字符占用 1 字节,“算法”中的每个字符占用 2 字节。我们可以通过高位填 0 将“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 字符在 Unicode 字符集中占据了前 128 个码点。也就是说,UTF-8 编码可以向下兼容 ASCII 码。这意味着我们可以使用 UTF-8 来解析年代久远的 ASCII 码文本。
    • 对于长度为 \\(n\\) 字节的字符(其中 \\(n > 1\\)),将首个字节的高 \\(n\\) 位都设置为 \\(1\\) ,第 \\(n + 1\\) 位设置为 \\(0\\) ;从第二个字节开始,将每个字节的高 2 位都设置为 \\(10\\) ;其余所有位用于填充字符的 Unicode 码点。

    图 3-8 展示了“Hello算法”对应的 UTF-8 编码。观察发现,由于最高 \\(n\\) 位都设置为 \\(1\\) ,因此系统可以通过读取最高位 \\(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\\) 个字符,我们需要从字符串的开始处遍历到第 \\(i\\) 个字符,这需要 \\(O(n)\\) 的时间。
    • 字符计数:与随机访问类似,计算 UTF-16 编码的字符串的长度也是 \\(O(1)\\) 的操作。但是,计算 UTF-8 编码的字符串的长度需要遍历整个字符串。
    • 字符串操作:在 UTF-16 编码的字符串上,很多字符串操作(如分割、连接、插入、删除等)更容易进行。在 UTF-8 编码的字符串上,进行这些操作通常需要额外的计算,以确保不会产生无效的 UTF-8 编码。

    实际上,编程语言的字符编码方案设计是一个很有趣的话题,涉及许多因素。

    • Java 的 String 类型使用 UTF-16 编码,每个字符占用 2 字节。这是因为 Java 语言设计之初,人们认为 16 位足以表示所有可能的字符。然而,这是一个不正确的判断。后来 Unicode 规范扩展到了超过 16 位,所以 Java 中的字符现在可能由一对 16 位的值(称为“代理对”)表示。
    • JavaScript 和 TypeScript 的字符串使用 UTF-16 编码的原因与 Java 类似。当 1995 年 Netscape 公司首次推出 JavaScript 语言时,Unicode 还处于发展早期,那时候使用 16 位的编码就足以表示所有的 Unicode 字符了。
    • C# 使用 UTF-16 编码,主要是因为 .NET 平台是由 Microsoft 设计的,而 Microsoft 的很多技术(包括 Windows 操作系统)都广泛使用 UTF-16 编码。

    由于以上编程语言对字符数量的低估,它们不得不采取“代理对”的方式来表示超过 16 位长度的 Unicode 字符。这是一个不得已为之的无奈之举。一方面,包含代理对的字符串中,一个字符可能占用 2 字节或 4 字节,从而丧失了等长编码的优势。另一方面,处理代理对需要额外增加代码,这提高了编程的复杂性和调试难度。

    出于以上原因,部分编程语言提出了一些不同的编码方案。

    • Python 中的 str 使用 Unicode 编码,并采用一种灵活的字符串表示,存储的字符长度取决于字符串中最大的 Unicode 码点。若字符串中全部是 ASCII 字符,则每个字符占用 1 字节;如果有字符超出了 ASCII 范围,但全部在基本多语言平面(BMP)内,则每个字符占用 2 字节;如果有超出 BMP 的字符,则每个字符占用 4 字节。
    • Go 语言的 string 类型在内部使用 UTF-8 编码。Go 语言还提供了 rune 类型,它用于表示单个 Unicode 码点。
    • Rust 语言的 strString 类型在内部使用 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 位的 \\(1\\) 会被舍弃。也就是说,负零的补码为 \\(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":"

    细心的你可能会发现:intfloat 长度相同,都是 4 字节 ,但为什么 float 的取值范围远大于 int ?这非常反直觉,因为按理说 float 需要表示小数,取值范围应该变小才对。

    实际上,这是因为浮点数 float 采用了不同的表示方式。记一个 32 比特长度的二进制数为:

    \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

    根据 IEEE 754 标准,32-bit 长度的 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   IEEE 754 标准下的 float 的计算示例

    观察图 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":"
    • 数据结构可以从逻辑结构和物理结构两个角度进行分类。逻辑结构描述了数据元素之间的逻辑关系,而物理结构描述了数据在计算机内存中的存储方式。
    • 常见的逻辑结构包括线性、树状和网状等。通常我们根据逻辑结构将数据结构分为线性(数组、链表、栈、队列)和非线性(树、图、堆)两种。哈希表的实现可能同时包含线性数据结构和非线性数据结构。
    • 当程序运行时,数据被存储在计算机内存中。每个内存空间都拥有对应的内存地址,程序通过这些内存地址访问数据。
    • 物理结构主要分为连续空间存储(数组)和分散空间存储(链表)。所有数据结构都是由数组、链表或两者的组合实现的。
    • 计算机中的基本数据类型包括整数 byteshortintlong ,浮点数 floatdouble ,字符 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:基于数组实现的数据结构也称“静态数据结构” 是否有歧义?栈也可以进行出栈和入栈等操作,这些操作都是“动态”的。

    栈确实可以实现动态的数据操作,但数据结构仍然是“静态”(长度不可变)的。尽管基于数组的数据结构可以动态地添加或删除元素,但它们的容量是固定的。如果数据量超出了预分配的大小,就需要创建一个新的更大的数组,并将旧数组的内容复制到新数组中。

    Q:在构建栈(队列)的时候,未指定它的大小,为什么它们是“静态数据结构”呢?

    在高级编程语言中,我们无须人工指定栈(队列)的初始容量,这个工作由类内部自动完成。例如,Java 的 ArrayList 的初始容量通常为 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 章   分治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"本章内容","text":"
    • 12.1   分治算法
    • 12.2   分治搜索策略
    • 12.3   构建树问题
    • 12.4   汉诺塔问题
    • 12.5   小结
    ","path":["第 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

    给定一个长度为 \\(n\\) 的有序数组 nums ,其中所有元素都是唯一的,请查找元素 target

    从分治角度,我们将搜索区间 \\([i, j]\\) 对应的子问题记为 \\(f(i, j)\\) 。

    以原问题 \\(f(0, n-1)\\) 为起始点,通过以下步骤进行二分查找。

    1. 计算搜索区间 \\([i, j]\\) 的中点 \\(m\\) ,根据它排除一半搜索区间。
    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":"

    原问题定义为从 preorderinorder 构建二叉树,是一个典型的分治问题。

    • 问题可以分解:从分治的角度切入,我们可以将原问题划分为两个子问题:构建左子树、构建右子树,加上一步操作:初始化根节点。而对于每棵子树(子问题),我们仍然可以复用以上划分方法,将其划分为更小的子树(子问题),直至达到最小子问题(空子树)时终止。
    • 子问题是独立的:左子树和右子树是相互独立的,它们之间没有交集。在构建左子树时,我们只需关注中序遍历和前序遍历中与左子树对应的部分。右子树同理。
    • 子问题的解可以合并:一旦得到了左子树和右子树(子问题的解),我们就可以将它们链接到根节点上,得到原问题的解。
    ","path":["第 12 章   分治","12.3   构建二叉树问题"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   如何划分子树","text":"

    根据以上分析,这道题可以使用分治来求解,但如何通过前序遍历 preorder 和中序遍历 inorder 来划分左子树和右子树呢?

    根据定义,preorderinorder 都可以划分为三个部分。

    • 前序遍历:[ 根节点 | 左子树 | 右子树 ] ,例如图 12-5 的树对应 [ 3 | 9 | 2 1 7 ]
    • 中序遍历:[ 左子树 | 根节点 | 右子树 ] ,例如图 12-5 的树对应 [ 9 | 3 | 1 2 7 ]

    以上图数据为例,我们可以通过图 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":"

    根据以上划分方法,我们已经得到根节点、左子树、右子树在 preorderinorder 中的索引区间。而为了描述这些索引区间,我们需要借助几个指针变量。

    • 将当前树的根节点在 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":"

    一方面,分治可以用来解决许多经典算法问题。

    • 寻找最近点对:该算法首先将点集分成两部分,然后分别找出两部分中的最近点对,最后找出跨越两部分的最近点对。
    • 大整数乘法:例如 Karatsuba 算法,它将大整数乘法分解为几个较小的整数的乘法和加法。
    • 矩阵乘法:例如 Strassen 算法,它将大矩阵乘法分解为多个小矩阵的乘法和加法。
    • 汉诺塔问题:汉诺塔问题可以通过递归解决,这是典型的分治策略应用。
    • 求解逆序对:在一个序列中,如果前面的数字大于后面的数字,那么这两个数字构成一个逆序对。求解逆序对问题可以利用分治的思想,借助归并排序进行求解。

    另一方面,分治在算法和数据结构的设计中应用得非常广泛。

    • 二分查找:二分查找是将有序数组从中点索引处分为两部分,然后根据目标值与中间元素值比较结果,决定排除哪一半区间,并在剩余区间执行相同的二分操作。
    • 归并排序:本节开头已介绍,不再赘述。
    • 快速排序:快速排序是选取一个基准值,然后把数组分为两个子数组,一个子数组的元素比基准值小,另一子数组的元素比基准值大,再对这两部分进行相同的划分操作,直至子数组只剩下一个元素。
    • 桶排序:桶排序的基本思想是将数据分散到多个桶,然后对每个桶内的元素进行排序,最后将各个桶的元素依次取出,从而得到一个有序数组。
    • 树:例如二叉搜索树、AVL 树、红黑树、B 树、B+ 树等,它们的查找、插入和删除等操作都可以视为分治策略的应用。
    • 堆:堆是一种特殊的完全二叉树,其各种操作,如插入、删除和堆化,实际上都隐含了分治的思想。
    • 哈希表:虽然哈希表并不直接应用分治,但某些哈希冲突解决方案间接应用了分治策略,例如,链式地址中的长链表会被转化为红黑树,以提升查询效率。

    可以看出,分治是一种“润物细无声”的算法思想,隐含在各种算法与数据结构之中。

    ","path":["第 12 章   分治","12.1   分治算法"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   汉诺塔问题","text":"

    在归并排序和构建二叉树中,我们都是将原问题分解为两个规模为原问题一半的子问题。然而对于汉诺塔问题,我们采用不同的分解策略。

    Question

    给定三根柱子,记为 ABC 。起始状态下,柱子 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)\\) 的过程可总结为:将两个圆盘借助 BA 移至 C 。其中,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 中剩余的一个圆盘从 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\\) 个圆盘借助 CA 移至 B
    2. 将剩余 \\(1\\) 个圆盘从 A 直接移至 C
    3. 将 \\(n-1\\) 个圆盘借助 AB 移至 C

    对于这两个子问题 \\(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) ,它的作用是将柱 src 顶部的 \\(i\\) 个圆盘借助缓冲柱 buf 移动至目标柱 tar

    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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    # 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar)\n    # 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    # 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    DFS(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    Move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfsHanota(i-1, src, tar, buf)\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar)\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* 求解汉诺塔问题 */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\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) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 src 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, src, tar);\n}\n\n/* 求解汉诺塔问题 */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n  dfs(i - 1, src, tar, buf);\n  // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n  move(src, tar);\n  // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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  // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move_pan(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\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) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar)\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n  dfs(i - 1, src, tar, buf)\n  # 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n  move(src, tar)\n  # 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n  dfs(i - 1, buf, src, tar)\nend\n\n### 求解汉诺塔问题 ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # 将 A 顶部 n 个圆盘借助 B 移到 C\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}\\) 秒,合约 \\(5850\\) 亿年,远远超过了现在对宇宙年龄的估计。所以,倘若这个传说是真的,我们应该不需要担心世界末日的到来。

    ","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 章   动态规划"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"本章内容","text":"
    • 14.1   初探动态规划
    • 14.2   DP 问题特性
    • 14.3   DP 解题思路
    • 14.4   0-1 背包问题
    • 14.5   完全背包问题
    • 14.6   编辑距离问题
    • 14.7   小结
    ","path":["第 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\\) ,则从地面爬到第 \\(3\\) 阶的最小代价为 \\(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 所示,爬上第 \\(3\\) 阶仅剩 \\(2\\) 种可行方案,其中连续三次跳 \\(1\\) 阶的方案不满足约束条件,因此被舍弃。

    图 14-8   带约束爬到第 3 阶的方案数量

    在该问题中,如果上一轮是跳 \\(1\\) 阶上来的,那么下一轮就必须跳 \\(2\\) 阶。这意味着,下一步选择不能由当前状态(当前所在楼梯阶数)独立决定,还和前一个状态(上一轮所在楼梯阶数)有关。

    不难发现,此问题已不满足无后效性,状态转移方程 \\(dp[i] = dp[i-1] + dp[i-2]\\) 也失效了,因为 \\(dp[i-1]\\) 代表本轮跳 \\(1\\) 阶,但其中包含了许多“上一轮是跳 \\(1\\) 阶上来的”方案,而为了满足约束,我们就不能将 \\(dp[i-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

    给定一个 \\(n \\times m\\) 的二维网格 grid ,网格中的每个单元格包含一个非负整数,表示该单元格的代价。机器人以左上角单元格为起始点,每次只能向下或者向右移动一步,直至到达右下角单元格。请返回从左上角到右下角的最小路径和。

    图 14-10 展示了一个例子,给定网格的最小路径和为 \\(13\\) 。

    图 14-10   最小路径和示例数据

    第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

    本题的每一轮的决策就是从当前格子向下或向右走一步。设当前格子的行列索引为 \\([i, j]\\) ,则向下或向右走一步后,索引变为 \\([i+1, j]\\) 或 \\([i, j+1]\\) 。因此,状态应包含行索引和列索引两个变量,记为 \\([i, j]\\) 。

    状态 \\([i, j]\\) 对应的子问题为:从起始点 \\([0, 0]\\) 走到 \\([i, j]\\) 的最小路径和,解记为 \\(dp[i, j]\\) 。

    至此,我们就得到了图 14-11 所示的二维 \\(dp\\) 矩阵,其尺寸与输入网格 \\(grid\\) 相同。

    图 14-11   状态定义与 dp 表

    Note

    动态规划和回溯过程可以描述为一个决策序列,而状态由所有决策变量构成。它应当包含描述解题进度的所有变量,其包含了足够的信息,能够用来推导出下一个状态。

    每个状态都对应一个子问题,我们会定义一个 \\(dp\\) 表来存储所有子问题的解,状态的每个独立变量都是 \\(dp\\) 表的一个维度。从本质上看,\\(dp\\) 表是状态和子问题的解之间的映射。

    第二步:找出最优子结构,进而推导出状态转移方程

    对于状态 \\([i, j]\\) ,它只能从上边格子 \\([i-1, j]\\) 和左边格子 \\([i, j-1]\\) 转移而来。因此最优子结构为:到达 \\([i, j]\\) 的最小路径和由 \\([i, j-1]\\) 的最小路径和与 \\([i-1, j]\\) 的最小路径和中较小的那一个决定。

    根据以上分析,可推出图 14-12 所示的状态转移方程:

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

    图 14-12   最优子结构与状态转移方程

    Note

    根据定义好的 \\(dp\\) 表,思考原问题和子问题的关系,找出通过子问题的最优解来构造原问题的最优解的方法,即最优子结构。

    一旦我们找到了最优子结构,就可以使用它来构建出状态转移方程。

    第三步:确定边界条件和状态转移顺序

    在本题中,处在首行的状态只能从其左边的状态得来,处在首列的状态只能从其上边的状态得来,因此首行 \\(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","level":3,"title":"1.   方法一:暴力搜索","text":"

    从状态 \\([i, j]\\) 开始搜索,不断分解为更小的状态 \\([i-1, j]\\) 和 \\([i, j-1]\\) ,递归函数包括以下要素。

    • 递归参数:状态 \\([i, j]\\) 。
    • 返回值:从 \\([0, 0]\\) 到 \\([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","level":3,"title":"2.   方法二:记忆化搜索","text":"

    我们引入一个和网格 grid 相同尺寸的记忆列表 mem ,用于记录各个子问题的解,并将重叠子问题进行剪枝:

    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","level":3,"title":"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":"

    编辑距离,也称 Levenshtein 距离,指两个字符串之间互相转换的最少修改次数,通常用于在信息检索和自然语言处理中度量两个序列的相似度。

    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":"

    第一步:思考每轮的决策,定义状态,从而得到 \\(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, j]\\) 对应的子问题:将 \\(s\\) 的前 \\(i\\) 个字符更改为 \\(t\\) 的前 \\(j\\) 个字符所需的最少编辑步数。

    至此,得到一个尺寸为 \\((i+1) \\times (j+1)\\) 的二维 \\(dp\\) 表。

    第二步:找出最优子结构,进而推导出状态转移方程

    考虑子问题 \\(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] \\]

    第三步:确定边界条件和状态转移顺序

    当两字符串都为空时,编辑步数为 \\(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-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","level":2,"title":"14.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 -1\\) 阶或第 \\(i - 2\\) 阶迈向第 \\(i\\) 阶。

    由此便可得出一个重要推论:爬到第 \\(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","level":2,"title":"14.1.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","level":2,"title":"14.1.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\\) 对应子问题的解。
    • 将最小子问题对应的状态(第 \\(1\\) 阶和第 \\(2\\) 阶楼梯)称为初始状态。
    • 将递推公式 \\(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\\) 轮决策组成的过程,对于每个物体都有不放入和放入两种决策,因此该问题满足决策树模型。

    该问题的目标是求解“在限定背包容量下能放入物品的最大价值”,因此较大概率是一个动态规划问题。

    第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

    对于每个物品来说,不放入背包,背包容量不变;放入背包,背包容量减小。由此可得状态定义:当前物品编号 \\(i\\) 和背包容量 \\(c\\) ,记为 \\([i, c]\\) 。

    状态 \\([i, c]\\) 对应的子问题为:前 \\(i\\) 个物品在容量为 \\(c\\) 的背包中的最大价值,记为 \\(dp[i, c]\\) 。

    待求解的是 \\(dp[n, cap]\\) ,因此需要一个尺寸为 \\((n+1) \\times (cap+1)\\) 的二维 \\(dp\\) 表。

    第二步:找出最优子结构,进而推导出状态转移方程

    当我们做出物品 \\(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\\) ,则只能选择不放入背包。

    第三步:确定边界条件和状态转移顺序

    当无物品或背包容量为 \\(0\\) 时最大价值为 \\(0\\) ,即首列 \\(dp[i, 0]\\) 和首行 \\(dp[0, c]\\) 都等于 \\(0\\) 。

    当前状态 \\([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","level":3,"title":"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","level":3,"title":"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","level":3,"title":"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 的第一维 \\(i\\) 直接删除,并且把内循环更改为倒序遍历即可:

    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()\\) 改为求和运算符。

    编辑距离问题

    • 编辑距离(Levenshtein 距离)用于衡量两个字符串之间的相似度,其定义为从一个字符串到另一个字符串的最少编辑步数,编辑操作包括添加、删除、替换。
    • 编辑距离问题的状态定义为将 \\(s\\) 的前 \\(i\\) 个字符更改为 \\(t\\) 的前 \\(j\\) 个字符所需的最少编辑步数。当 \\(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":"

    对比两道题目的代码,状态转移中有一处从 \\(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":"

    零钱兑换可以看作完全背包问题的一种特殊情况,两者具有以下联系与不同点。

    • 两道题可以相互转换,“物品”对应“硬币”、“物品重量”对应“硬币面值”、“背包容量”对应“目标金额”。
    • 优化目标相反,完全背包问题是要最大化物品价值,零钱兑换问题是要最小化硬币数量。
    • 完全背包问题是求“不超过”背包容量下的解,零钱兑换是求“恰好”凑到目标金额的解。

    第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

    状态 \\([i, a]\\) 对应的子问题为:前 \\(i\\) 种硬币能够凑出金额 \\(a\\) 的最少硬币数量,记为 \\(dp[i, a]\\) 。

    二维 \\(dp\\) 表的尺寸为 \\((n+1) \\times (amt+1)\\) 。

    第二步:找出最优子结构,进而推导出状态转移方程

    本题与完全背包问题的状态转移方程存在以下两点差异。

    • 本题要求最小值,因此需将运算符 \\(\\max()\\) 更改为 \\(\\min()\\) 。
    • 优化主体是硬币数量而非商品价值,因此在选中硬币时执行 \\(+1\\) 即可。
    \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

    第三步:确定边界条件和状态转移顺序

    当目标金额为 \\(0\\) 时,凑出它的最少硬币数量为 \\(0\\) ,即首列所有 \\(dp[i, 0]\\) 都等于 \\(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]\\) 都初始化为 \\(1\\) 。当无硬币时,无法凑出任何 \\(>0\\) 的目标金额,因此首行所有 \\(dp[0, a]\\) 都等于 \\(0\\) 。

    ","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 章   图"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"本章内容","text":"
    • 9.1   图
    • 9.2   图基础操作
    • 9.3   图的遍历
    • 9.4   小结
    ","path":["第 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 所示。

    • 在无向图中,边表示两顶点之间的“双向”连接关系,例如微信或 QQ 中的“好友关系”。
    • 在有向图中,边具有方向性,即 \\(A \\rightarrow B\\) 和 \\(A \\leftarrow B\\) 两个方向的边是相互独立的,例如微博或抖音上的“关注”与“被关注”关系。

    图 9-2   有向图与无向图

    根据所有顶点是否连通,可分为连通图(connected graph)和非连通图(disconnected graph),如图 9-3 所示。

    • 对于连通图,从某个顶点出发,可以到达其余任意顶点。
    • 对于非连通图,从某个顶点出发,至少有一个顶点无法到达。

    图 9-3   连通图与非连通图

    我们还可以为边添加“权重”变量,从而得到如图 9-4 所示的有权图(weighted graph)。例如在《王者荣耀》等手游中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以用有权图来表示。

    图 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\\) 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 \\(1\\) 或 \\(0\\) 表示两个顶点之间是否存在边。

    如图 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)\\) 时间。而由于是无向图,因此需要同时更新两个方向的边。
    • 添加顶点:在邻接矩阵的尾部添加一行一列,并全部填 \\(0\\) 即可,使用 \\(O(n)\\) 时间。
    • 删除顶点:在邻接矩阵中删除一行一列。当删除首行首列时达到最差情况,需要将 \\((n-1)^2\\) 个元素“向左上移动”,从而使用 \\(O(n^2)\\) 时间。
    • 初始化:传入 \\(n\\) 个顶点,初始化长度为 \\(n\\) 的顶点列表 vertices ,使用 \\(O(n)\\) 时间;初始化 \\(n \\times n\\) 大小的邻接矩阵 adjMat ,使用 \\(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\\) 的顶点,则需遍历整个邻接表,将所有大于 \\(i\\) 的索引全部减 \\(1\\) ,效率很低。而如果每个顶点都是唯一的 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 的哈希表,它可以在 \\(O(1)\\) 时间复杂度下进行 key 的增删查改操作。根据 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 操作会返回新的的引用,必须让原引用重新赋值为新slice的引用\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   图的深度优先遍历步骤

    深度优先遍历的序列是否唯一?

    与广度优先遍历类似,深度优先遍历序列的顺序也不是唯一的。给定某顶点,先往哪个方向探索都可以,即邻接顶点的顺序可以任意打乱,都是深度优先遍历。

    以树的遍历为例,“根 \\(\\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":"
    • 图由顶点和边组成,可以表示为一组顶点和一组边构成的集合。
    • 相较于线性关系(链表)和分治关系(树),网络关系(图)具有更高的自由度,因而更为复杂。
    • 有向图的边具有方向性,连通图中的任意顶点均可达,有权图的每条边都包含权重变量。
    • 邻接矩阵利用矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 \\(1\\) 或 \\(0\\) 表示两个顶点之间有边或无边。邻接矩阵在增删查改操作上效率很高,但空间占用较多。
    • 邻接表使用多个链表来表示图,第 \\(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 章   贪心"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"本章内容","text":"
    • 15.1   贪心算法
    • 15.2   分数背包问题
    • 15.3   最大容量问题
    • 15.4   最大切分乘积问题
    • 15.5   小结
    ","path":["第 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(\\log n)\\) ,空间复杂度通常为 \\(O(\\log n)\\) 或 \\(O(n)\\) ,取决于编程语言的具体实现。

    除排序之外,在最差情况下,需要遍历整个物品列表,因此时间复杂度为 \\(O(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
    可视化运行

    全屏观看 >

    你可能会不由地发出感叹:So clean !贪心算法仅用约十行代码就解决了零钱兑换问题。

    ","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":"

    贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下列举了一些典型的贪心算法问题。

    • 硬币找零问题:在某些硬币组合下,贪心算法总是可以得到最优解。
    • 区间调度问题:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。
    • 分数背包问题:给定一组物品和一个载重量,你的目标是选择一组物品,使得总重量不超过载重量,且总价值最大。如果每次都选择性价比最高(价值 / 重量)的物品,那么贪心算法在一些情况下可以得到最优解。
    • 股票买卖问题:给定一组股票的历史价格,你可以进行多次买卖,但如果你已经持有股票,那么在卖出之前不能再买,目标是获取最大利润。
    • 霍夫曼编码:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最低的两个节点合并,最后得到的霍夫曼树的带权路径长度(编码长度)最小。
    • Dijkstra 算法:它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法。
    ","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\\) 的整数都应该被切分。

    贪心策略一:如果切分方案中包含 \\(\\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\\) 总是可以替换为两个 \\(3\\) ,从而获得更大的乘积。

    图 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 所示,我们无须通过循环来切分整数,而可以利用向下整除运算得到 \\(3\\) 的个数 \\(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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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() 内部调用 C 语言库的 pow() 函数,其执行浮点取幂,时间复杂度为 \\(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\\) :假设最优切分方案中存在 \\(\\geq 4\\) 的因子 \\(x\\) ,那么一定可以将其继续划分为 \\(2(x-2)\\) ,从而获得更大(或相等)的乘积。这与假设矛盾。
    2. 切分方案不包含 \\(1\\) :假设最优切分方案中存在一个因子 \\(1\\) ,那么它一定可以合并入另外一个因子中,以获得更大的乘积。这与假设矛盾。
    3. 切分方案最多包含两个 \\(2\\) :假设最优切分方案中包含三个 \\(2\\) ,那么一定可以替换为两个 \\(3\\) ,乘积更大。这与假设矛盾。
    ","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 章   哈希表"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"本章内容","text":"
    • 6.1   哈希表
    • 6.2   哈希冲突
    • 6.3   哈希算法
    • 6.4   小结
    ","path":["第 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 码累积到哈希值中。
    • 异或哈希:将输入数据的每个元素通过异或操作累积到一个哈希值中。
    • 旋转哈希:将每个字符的 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    \"\"\"异或哈希\"\"\"\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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### 异或哈希 ###\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\\) 整除,那么所有可以被 \\(3\\) 整除的 key 都会被映射到 \\(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\\) ,由于 keymodulus 之间不存在公约数,因此输出的哈希值的均匀性会明显提升。

    \\[ \\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":"

    不难发现,以上介绍的简单哈希算法都比较“脆弱”,远远没有达到哈希算法的设计目标。例如,由于加法和异或满足交换律,因此加法哈希和异或哈希无法区分内容相同但顺序不同的字符串,这可能会加剧哈希冲突,并引起一些安全问题。

    在实际中,我们通常会用一些标准哈希算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。

    近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。表 6-2 展示了在实际应用中常见的哈希算法。

    • MD5 和 SHA-1 已多次被成功攻击,因此它们被各类安全应用弃用。
    • SHA-2 系列中的 SHA-256 是最安全的哈希算法之一,仍未出现成功的攻击案例,因此常用在各类安全应用与协议中。
    • 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 算法\"\nhash_str = hash(str)\n# 字符串“Hello 算法”的哈希值为 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 算法\";\nsize_t hashStr = hash<string>()(str);\n// 字符串“Hello 算法”的哈希值为 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 算法\";\nint hashStr = str.hashCode();\n// 字符串“Hello 算法”的哈希值为 -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 算法\";\nint hashStr = str.GetHashCode();\n// 字符串“Hello 算法”的哈希值为 -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 算法\"\nlet hashStr = str.hashValue\n// 字符串“Hello 算法”的哈希值为 -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 算法\";\nint hashStr = str.hashCode;\n// 字符串“Hello 算法”的哈希值为 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 算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 字符串“Hello 算法”的哈希值为 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 算法\"\nval hashStr = str.hashCode()\n// 字符串“Hello 算法”的哈希值为 -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 算法\"\nhash_str = str.hash\n# 字符串“Hello 算法”的哈希值为 -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
    可视化运行

    全屏观看 >

    在许多编程语言中,只有不可变对象才可作为哈希表的 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 来标记这个桶。在该机制下,NoneTOMBSTONE 都代表空桶,都可以放置键值对。但不同的是,线性探测到 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 个键值对,超出容量则连接一个溢出桶;当溢出桶过多时,会执行一次特殊的等量扩容操作,以确保性能。
    ","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
    可视化运行

    全屏观看 >

    哈希表有三种常用的遍历方式:遍历键值对、遍历键和遍历值。示例代码如下:

    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
    可视化运行

    全屏观看 >

    ","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 对应的键值对在数组中的存储位置。

    输入一个 key ,哈希函数的计算过程分为以下两步。

    1. 通过某种哈希算法 hash() 计算得到哈希值。
    2. 将哈希值对桶数量(数组长度)capacity 取模,从而获取该 key 对应的桶(数组索引)index
    index = hash(key) % capacity\n

    随后,我们就可以利用 index 在哈希表中访问对应的桶,从而获取 value

    设数组长度 capacity = 100、哈希算法 hash(key) = key ,易得哈希函数为 key % 100 。图 6-2 以 key 学号和 value 姓名为例,展示了哈希函数的工作原理。

    图 6-2   哈希函数工作原理

    以下代码实现了一个简单哈希表。其中,我们将 keyvalue 封装成一个类 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 ,哈希表能够在 \\(O(1)\\) 时间内查询到 value ,效率非常高。
    • 常见的哈希表操作包括查询、添加键值对、删除键值对和遍历哈希表等。
    • 哈希函数将 key 映射为数组索引,从而访问对应桶并获取 value
    • 两个不同的 key 可能在经过哈希函数后得到相同的数组索引,导致查询结果出错,这种现象被称为哈希冲突。
    • 哈希表容量越大,哈希冲突的概率就越低。因此可以通过扩容哈希表来缓解哈希冲突。与数组扩容类似,哈希表扩容操作的开销很大。
    • 负载因子定义为哈希表中元素数量除以桶数量,反映了哈希冲突的严重程度,常用作触发哈希表扩容的条件。
    • 链式地址通过将单个元素转化为链表,将所有冲突元素存储在同一个链表中。然而,链表过长会降低查询效率,可以通过进一步将链表转换为红黑树来提高效率。
    • 开放寻址通过多次探测来处理哈希冲突。线性探测使用固定步长,缺点是不能删除元素,且容易产生聚集。多次哈希使用多个哈希函数进行探测,相较线性探测更不易产生聚集,但多个哈希函数增加了计算量。
    • 不同编程语言采取了不同的哈希表实现。例如,Java 的 HashMap 使用链式地址,而 Python 的 Dict 采用开放寻址。
    • 在哈希表中,我们希望哈希算法具有确定性、高效率和均匀分布的特点。在密码学中,哈希算法还应该具备抗碰撞性和雪崩效应。
    • 哈希算法通常采用大质数作为模数,以最大化地保证哈希值均匀分布,减少哈希冲突。
    • 常见的哈希算法包括 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 章   堆"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"本章内容","text":"
    • 8.1   堆
    • 8.2   建堆操作
    • 8.3   Top-k 问题
    • 8.4   小结
    ","path":["第 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    # 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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        // 堆化除叶节点以外的其他所有节点\n        h.siftDown(i)\n    }\n    return h\n}\n
    my_heap.swift
    /* 构造方法,根据输入列表建堆 */\ninit(nums: [Int]) {\n    // 将列表元素原封不动添加进堆\n    maxHeap = nums\n    // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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  // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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        // 堆化除叶节点以外的其他所有节点\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        // 堆化除叶节点以外的其他所有节点\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            // 当“越过根节点”或“节点无须修复”时,结束堆化\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  # 堆化除叶节点以外的其他所有节点\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)\\) 。但这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的性质。

    接下来我们来进行更为准确的计算。为了降低计算难度,假设给定一个节点数量为 \\(n\\) 、高度为 \\(h\\) 的“完美二叉树”,该假设不会影响计算结果的正确性。

    图 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() 访问堆顶元素(对于大 / 小顶堆分别为最大 / 小值) \\(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# Python 的 heapq 模块默认实现小顶堆\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// 初始化大顶堆(使用 lambda 表达式修改 Comparator 即可)\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// 初始化大顶堆(使用 lambda 表达式修改 Comparer 即可)\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// Swift 的 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// 初始化大顶堆(使用 lambda 表达式修改 Comparator 即可)\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
    可视化运行

    全屏观看 >

    ","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 可能大于堆中其他元素,堆的成立条件可能已被破坏,因此需要修复从插入节点到根节点的路径上的各个节点,这个操作被称为堆化(heapify)。

    考虑从入堆节点开始,从底至顶执行堆化。如图 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        # 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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    // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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    # 当“越过根节点”或“节点无须修复”时,结束堆化\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

    给定一个长度为 \\(n\\) 的无序数组 nums ,请返回数组中最大的 \\(k\\) 个元素。

    对于该问题,我们先介绍两种思路比较直接的解法,再介绍效率更高的堆解法。

    ","path":["第 8 章   堆","8.3   Top-k 问题"],"tags":[]},{"location":"chapter_heap/top_k/#831","level":2,"title":"8.3.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","level":2,"title":"8.3.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","level":2,"title":"8.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":"

    几年前,我在力扣上分享了“剑指 Offer”系列题解,受到了许多读者的鼓励和支持。在与读者交流期间,我最常被问的一个问题是“如何入门算法”。逐渐地,我对这个问题产生了浓厚的兴趣。

    两眼一抹黑地刷题似乎是最受欢迎的方法,简单、直接且有效。然而刷题就如同玩“扫雷”游戏,自学能力强的人能够顺利将地雷逐个排掉,而基础不足的人很可能被炸得满头是包,并在挫折中步步退缩。通读教材也是一种常见做法,但对于面向求职的人来说,毕业论文、投递简历、准备笔试和面试已经消耗了大部分精力,啃厚重的书往往变成了一项艰巨的挑战。

    如果你也面临类似的困扰,那么很幸运这本书“找”到了你。本书是我对这个问题给出的答案,即使不是最优解,也至少是一次积极的尝试。本书虽然不足以让你直接拿到 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 章   初识算法"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"本章内容","text":"
    • 1.1   算法无处不在
    • 1.2   算法是什么
    • 1.3   小结
    ","path":["第 1 章   初识算法"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   算法无处不在","text":"

    当我们听到“算法”这个词时,很自然地会想到数学。然而实际上,许多算法并不涉及复杂数学,而是更多地依赖基本逻辑,这些逻辑在我们的日常生活中处处可见。

    在正式探讨算法之前,有一个有趣的事实值得分享:你已经在不知不觉中学会了许多算法,并习惯将它们应用到日常生活中了。下面我将举几个具体的例子来证实这一点。

    例一:查字典。在字典里,每个汉字都对应一个拼音,而字典是按照拼音字母顺序排列的。假设我们需要查找一个拼音首字母为 \\(r\\) 的字,通常会按照图 1-1 所示的方式实现。

    1. 翻开字典约一半的页数,查看该页的首字母是什么,假设首字母为 \\(m\\) 。
    2. 由于在拼音字母表中 \\(r\\) 位于 \\(m\\) 之后,所以排除字典前半部分,查找范围缩小到后半部分。
    3. 不断重复步骤 1. 和步骤 2. ,直至找到拼音首字母为 \\(r\\) 的页码为止。
    <1><2><3><4><5>

    图 1-1   查字典步骤

    查字典这个小学生必备技能,实际上就是著名的“二分查找”算法。从数据结构的角度,我们可以把字典视为一个已排序的“数组”;从算法的角度,我们可以将上述查字典的一系列操作看作“二分查找”。

    例二:整理扑克。我们在打牌时,每局都需要整理手中的扑克牌,使其从小到大排列,实现流程如图 1-2 所示。

    1. 将扑克牌划分为“有序”和“无序”两部分,并假设初始状态下最左 1 张扑克牌已经有序。
    2. 在无序部分抽出一张扑克牌,插入至有序部分的正确位置;完成后最左 2 张扑克已经有序。
    3. 不断循环步骤 2. ,每一轮将一张扑克牌从无序部分插入至有序部分,直至所有扑克牌都有序。

    图 1-2   扑克排序步骤

    上述整理扑克牌的方法本质上是“插入排序”算法,它在处理小型数据集时非常高效。许多编程语言的排序库函数中都有插入排序的身影。

    例三:货币找零。假设我们在超市购买了 \\(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_paperbook/","level":1,"title":"纸质书","text":"

    经过长时间的打磨,《Hello 算法》纸质书终于发布了!此时的心情可以用一句诗来形容:

    追风赶月莫停留,平芜尽处是春山。

    以下视频展示了纸质书,并且包含我的一些思考:

    • 学习数据结构与算法的重要性。
    • 为什么在纸质书中选择 Python。
    • 对知识分享的理解。

    新人 UP 主,请多多关照、一键三连~谢谢!

    附纸质书快照:

    ","path":["纸质书"],"tags":[]},{"location":"chapter_paperbook/#_2","level":2,"title":"优势与不足","text":"

    总结一下纸质书可能会给大家带来惊喜的地方:

    • 采用全彩印刷,能够原汁原味地发挥出本书“动画图解”的优势。
    • 考究纸张材质,既保证色彩高度还原,也保留纸质书特有的质感。
    • 纸质版比网页版的格式更加规范,例如图中的公式使用斜体。
    • 在不提升定价的前提下,附赠思维导图折页、书签。
    • 纸质书、网页版、PDF 版内容同步,随意切换阅读。

    Tip

    由于纸质书和网页版的同步难度较大,因此可能会有一些细节上的不同,请您见谅!

    当然,纸质书也有一些值得大家入手前考虑的地方:

    • 使用 Python 语言,可能不匹配你的主语言(可以把 Python 看作伪代码,重在理解思路)。
    • 全彩印刷虽然大幅提升了图解和代码的阅读体验,但价格会比黑白印刷高一些。

    Tip

    “印刷质量”和“价格”就像算法中的“时间效率”和“空间效率”,难以两全。而我认为,“印刷质量”对应的是“时间效率”,更应该被注重。

    ","path":["纸质书"],"tags":[]},{"location":"chapter_paperbook/#_3","level":2,"title":"购买链接","text":"

    如果你对纸质书感兴趣,可以考虑入手一本。我们为大家争取到了新书 5 折优惠,请见此链接或扫描以下二维码:

    ","path":["纸质书"],"tags":[]},{"location":"chapter_paperbook/#_4","level":2,"title":"尾记","text":"

    起初,我低估了纸质书出版的工作量,以为只要维护好了开源项目,纸质版就可以通过某些自动化手段生成出来。实践证明,纸质书的生产流程与开源项目的更新机制存在很大的不同,两者之间的转化需要做许多额外工作。

    一本书的初稿与达到出版标准的定稿之间仍有较长距离,需要出版社(策划、编辑、设计、市场等)与作者的通力合作、长期雕琢。在此感谢图灵策划编辑王军花、以及人民邮电出版社和图灵社区每位参与本书出版流程的工作人员!

    希望这本书能够帮助到你!

    ","path":["纸质书"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"第 0 章   前言","text":"

    Abstract

    算法犹如美妙的交响乐,每一行代码都像韵律般流淌。

    愿这本书在你的脑海中轻轻响起,留下独特而深刻的旋律。

    ","path":["第 0 章   前言"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"本章内容","text":"
    • 0.1   关于本书
    • 0.2   如何使用本书
    • 0.3   小结
    ","path":["第 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 开发。感谢他的贡献,为读者提供了更灵活的阅读方式。

    在本书的创作过程中,我得到了许多人的帮助。

    • 感谢我在公司的导师李汐博士,在一次畅谈中你鼓励我“快行动起来”,坚定了我写这本书的决心;
    • 感谢我的女朋友泡泡作为本书的首位读者,从算法小白的角度提出许多宝贵建议,使得本书更适合新手阅读;
    • 感谢腾宝、琦宝、飞宝为本书起了一个富有创意的名字,唤起大家写下第一行代码“Hello World!”的美好回忆;
    • 感谢校铨在知识产权方面提供的专业帮助,这对本开源书的完善起到了重要作用;
    • 感谢苏潼为本书设计了精美的封面和 logo ,并在我的强迫症的驱使下多次耐心修改;
    • 感谢 @squidfunk 提供的排版建议,以及他开发的开源文档主题 Material-for-MkDocs 。

    在写作过程中,我阅读了许多关于数据结构与算法的教材和文章。这些作品为本书提供了优秀的范本,确保了本书内容的准确性与品质。在此感谢所有老师和前辈的杰出贡献!

    本书倡导手脑并用的学习方式,在这一点上我深受《动手学深度学习》的启发。在此向各位读者强烈推荐这本优秀的著作。

    衷心感谢我的父母,正是你们一直以来的支持与鼓励,让我有机会做这件富有趣味的事。

    ","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   运行代码示例

    运行代码的前置工作主要分为三步。

    第一步:安装本地编程环境。请参照附录所示的教程进行安装,如果已安装,则可跳过此步骤。

    第二步:克隆或下载代码仓库。前往 GitHub 仓库。如果已经安装 Git ,可以通过以下命令克隆本仓库:

    git clone https://github.com/krahets/hello-algo.git\n

    当然,你也可以在图 0-4 所示的位置,点击“Download ZIP”按钮直接下载代码压缩包,然后在本地解压即可。

    图 0-4   克隆仓库与下载代码

    第三步:运行源代码。如图 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. 阶段一:算法入门。我们需要熟悉各种数据结构的特点和用法,学习不同算法的原理、流程、用途和效率等方面的内容。
    2. 阶段二:刷算法题。建议从热门题目开刷,先积累至少 100 道题目,熟悉主流的算法问题。初次刷题时,“知识遗忘”可能是一个挑战,但请放心,这是很正常的。我们可以按照“艾宾浩斯遗忘曲线”来复习题目,通常在进行 3~5 轮的重复后,就能将其牢记在心。推荐的题单和刷题计划请见此 GitHub 仓库。
    3. 阶段三:搭建知识体系。在学习方面,我们可以阅读算法专栏文章、解题框架和算法教材,以不断丰富知识体系。在刷题方面,可以尝试采用进阶刷题策略,如按专题分类、一题多解、一解多题等,相关的刷题心得可以在各个社区找到。

    如图 0-8 所示,本书内容主要涵盖“阶段一”,旨在帮助你更高效地展开阶段二和阶段三的学习。

    图 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, et al. Introduction to Algorithms (3rd Edition).

    [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

    [3] Robert Sedgewick, et al. Algorithms (4th Edition).

    [4] 严蔚敏. 数据结构(C 语言版).

    [5] 邓俊辉. 数据结构(C++ 语言版,第三版).

    [6] 马克 艾伦 维斯著,陈越译. 数据结构与算法分析:Java语言描述(第三版).

    [7] 程杰. 大话数据结构.

    [8] 王争. 数据结构与算法之美.

    [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

    [10] Aston Zhang, et al. Dive into Deep Learning.

    ","path":["参考文献"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章   搜索","text":"

    Abstract

    搜索是一场未知的冒险,我们或许需要走遍神秘空间的每个角落,又或许可以快速锁定目标。

    在这场寻觅之旅中,每一次探索都可能得到一个未曾料想的答案。

    ","path":["第 10 章   搜索"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"本章内容","text":"
    • 10.1   二分查找
    • 10.2   二分查找插入点
    • 10.3   二分查找边界
    • 10.4   哈希优化策略
    • 10.5   重识搜索算法
    • 10.6   小结
    ","path":["第 10 章   搜索"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   二分查找","text":"

    二分查找(binary search)是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。

    Question

    给定一个长度为 \\(n\\) 的数组 nums ,元素按从小到大的顺序排列且不重复。请查找并返回元素 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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 次除法、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

    给定一个长度为 \\(n\\) 的有序数组 nums ,其中可能包含重复元素。请返回数组中最左一个元素 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 的元素。

    因此,如图 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

    给定一个长度为 \\(n\\) 的有序数组 nums 和一个元素 target ,数组不存在重复元素。现将 target 插入数组 nums 中,并保持其有序性。若数组中已存在元素 target ,则插入到其左方。请返回插入后 target 在数组中的索引。示例如图 10-4 所示。

    图 10-4   二分查找插入点示例数据

    如果想复用上一节的二分查找代码,则需要回答以下两个问题。

    问题一:当数组中包含 target 时,插入点的索引是否是该元素的索引?

    题目要求将 target 插入到相等元素的左边,这意味着新插入的 target 替换了原来 target 的位置。也就是说,当数组包含 target 时,插入点的索引就是该 target 的索引。

    问题二:当数组中不存在 target 时,插入点是哪个元素的索引?

    进一步思考二分查找过程:当 nums[m] < target 时 \\(i\\) 移动,这意味着指针 \\(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

    题目要求将目标元素插入到最左边,所以我们需要查找数组中最左一个 target 的索引。初步考虑通过图 10-5 所示的步骤实现。

    1. 执行二分查找,得到任意一个 target 的索引,记为 \\(k\\) 。
    2. 从索引 \\(k\\) 开始,向左进行线性遍历,当找到最左边的 target 时返回。

    图 10-5   线性查找重复元素的插入点

    此方法虽然可用,但其包含线性查找,因此时间复杂度为 \\(O(n)\\) 。当数组中存在很多重复的 target 时,该方法效率很低。

    现考虑拓展二分查找代码。如图 10-6 所示,整体流程保持不变,每轮先计算中点索引 \\(m\\) ,再判断 targetnums[m] 的大小关系,分为以下几种情况。

    • nums[m] < targetnums[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] > targetnums[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    \"\"\"方法一:暴力枚举\"\"\"\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一: 暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    ### 方法一:暴力枚举 ###\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    \"\"\"方法二:辅助哈希表\"\"\"\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二: 辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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/* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    ### 方法二:辅助哈希表 ###\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 章   排序"],"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 章   排序"],"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_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":"

    桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 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 + 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":"

    先来看一个简单的例子。给定一个长度为 \\(n\\) 的数组 nums ,其中的元素都是“非负整数”,计数排序的整体流程如图 11-16 所示。

    1. 遍历数组,找出其中的最大数字,记为 \\(m\\) ,然后创建一个长度为 \\(m + 1\\) 的辅助数组 counter
    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    # 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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  // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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  # 使用结果数组 res 覆盖原数组 nums\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)\\)、非原地排序:借助了长度分别为 \\(n\\) 和 \\(m\\) 的数组 rescounter
    • 稳定排序:由于向 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. 从堆顶元素开始,从顶到底执行堆化操作(sift down)。完成堆化后,堆的性质得到修复。
    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    # 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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  // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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  # 建堆操作:堆化除叶节点以外的其他所有节点\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. 初始状态下,数组的第 1 个元素已完成排序。
    2. 选取数组的第 2 个元素作为 base ,将其插入到正确位置后,数组的前 2 个元素已排序。
    3. 选取第 3 个元素作为 base ,将其插入到正确位置后,数组的前 3 个元素已排序。
    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. 选取数组最左端元素作为基准数,初始化两个指针 ij 分别指向数组的两端。
    2. 设置一个循环,在每轮中使用 ij)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。
    3. 循环执行步骤 2. ,直到 ij 相遇时停止,最后将基准数交换至两个子数组的分界线。
    <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 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 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   基数排序算法流程

    下面剖析代码实现。对于一个 \\(d\\) 进制的数字 \\(x\\) ,要获取其第 \\(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    \"\"\"获取元素 num 的第 k 位,其中 exp = 10^(k-1)\"\"\"\n    # 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"计数排序(根据 nums 第 k 位排序)\"\"\"\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)  # 获取 nums[i] 第 k 位,记为 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  # 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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) // 获取 nums[i] 第 k 位,记为 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 // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    (num / exp) % 10\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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) // 获取 nums[i] 第 k 位,记为 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 // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 _num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n  return (_num ~/ exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return ((num / exp) % 10) as usize;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 初始化为 0 以支持后续内存释放\n    // 统计 0~9 各数字的出现次数\n    for (int i = 0; i < size; i++) {\n        // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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) // 获取 nums[i] 第 k 位,记为 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 // 获取 d 在数组中的索引 j\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
    ### 获取元素 num 的第 k 位,其中 exp = 10^(k-1) ###\ndef digit(num, exp)\n  # 转入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n  (num / exp) % 10\nend\n\n### 计数排序(根据 nums 第 k 位排序)###\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) # 获取 nums[i] 第 k 位,记为 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 # 获取 d 在数组中的索引 j\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)\\)、非原地排序:与计数排序相同,基数排序需要借助长度为 \\(n\\) 和 \\(d\\) 的数组 rescounter
    • 稳定排序:当计数排序稳定时,基数排序也稳定;当计数排序不稳定时,基数排序无法保证得到正确的排序结果。
    ","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\\) 处的元素交换。完成后,数组前 1 个元素已排序。
    3. 选取区间 \\([1, n-1]\\) 中的最小元素,将其与索引 \\(1\\) 处的元素交换。完成后,数组前 2 个元素已排序。
    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":"

    运行效率:我们期望排序算法的时间复杂度尽量低,且总体操作数量较少(时间复杂度中的常数项变小)。对于大数据量的情况,运行效率显得尤为重要。

    就地性:顾名思义,原地排序通过在原数组上直接操作实现排序,无须借助额外的辅助数组,从而节省内存。通常情况下,原地排序的数据搬运操作较少,运行速度也更快。

    稳定性:稳定排序在完成排序后,相等元素在数组中的相对顺序不发生改变。

    稳定排序是多级排序场景的必要条件。假设我们有一个存储学生信息的表格,第 1 列和第 2 列分别是姓名和年龄。在这种情况下,非稳定排序可能导致输入数据的有序性丧失:

    # 输入数据是按照姓名排序好的\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-q-a","level":3,"title":"2.   Q & A","text":"

    Q:排序算法稳定性在什么情况下是必需的?

    在现实中,我们有可能基于对象的某个属性进行排序。例如,学生有姓名和身高两个属性,我们希望实现一个多级排序:先按照姓名进行排序,得到 (A, 180) (B, 185) (C, 170) (D, 170) ;再对身高进行排序。由于排序算法不稳定,因此可能得到 (D, 170) (C, 170) (A, 180) (B, 185)

    可以发现,学生 D 和 C 的位置发生了交换,姓名的有序性被破坏了,而这是我们不希望看到的。

    Q:哨兵划分中“从右往左查找”与“从左往右查找”的顺序可以交换吗?

    不行,当我们以最左端元素为基准数时,必须先“从右往左查找”再“从左往右查找”。这个结论有些反直觉,我们来剖析一下原因。

    哨兵划分 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] 为基准数,那么正好反过来,必须先“从左往右查找”。

    Q:关于快速排序的递归深度优化,为什么选短的数组能保证递归深度不超过 \\(\\log n\\) ?

    递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在递归深度优化后,向下递归的子数组长度最大为原数组长度的一半。假设最差情况,一直为一半长度,那么最终的递归深度就是 \\(\\log n\\) 。

    回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) ,递归深度为 \\(n\\) 。递归深度优化可以避免这种情况出现。

    Q:当数组中所有元素都相等时,快速排序的时间复杂度是 \\(O(n^2)\\) 吗?该如何处理这种退化情况?

    是的。对于这种情况,可以考虑通过哨兵划分将数组划分为三个部分:小于、等于、大于基准数。仅向下递归小于和大于的两部分。在该方法下,输入元素全部相等的数组,仅一轮哨兵划分即可完成排序。

    Q:桶排序的最差时间复杂度为什么是 \\(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 章   栈与队列"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"本章内容","text":"
    • 5.1   栈
    • 5.2   队列
    • 5.3   双向队列
    • 5.4   小结
    ","path":["第 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
    可视化运行

    全屏观看 >

    ","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":"

    回顾上一节内容,我们使用普通单向链表来实现队列,因为它可以方便地删除头节点(对应出队操作)和在尾节点后添加新节点(对应入队操作)。

    对于双向队列而言,头部和尾部都可以执行入队和出队操作。换句话说,双向队列需要实现另一个对称方向的操作。为此,我们采用“双向链表”作为双向队列的底层数据结构。

    如图 5-8 所示,我们将双向链表的头节点和尾节点视为双向队列的队首和队尾,同时实现在两端添加和删除节点的功能。

    <1><2><3><4><5>

    图 5-8   基于链表实现双向队列的入队出队操作

    实现代码如下所示:

    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   基于数组实现双向队列的入队出队操作

    在队列的实现基础上,仅需增加“队首入队”和“队尾出队”的方法:

    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;        // 队首指针,指向队首元素\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; // 队首指针,指向队首元素\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;   // 队首指针,指向队首元素\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   // 队首指针,指向队首元素\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 // 队首指针,指向队首元素\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; // 队首指针,指向队首元素\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; // 队首指针,指向队首元素\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; // 队首指针,指向队首元素\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,    // 队首指针,指向队首元素\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;       // 队首指针,指向队首元素\n    int queSize;     // 尾指针,指向队尾 + 1\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 // 队首指针,指向队首元素\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":"

    双向队列兼具栈与队列的逻辑,因此它可以实现这两者的所有应用场景,同时提供更高的自由度。

    我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 push 到栈中,然后通过 pop 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 \\(50\\) 步)。当栈的长度超过 \\(50\\) 时,软件需要在栈底(队首)执行删除操作。但栈无法实现该功能,此时就需要使用双向队列来替代栈。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。

    ","path":["第 5 章   栈与队列","5.3   双向队列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   队列","text":"

    队列(queue)是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。

    如图 5-4 所示,我们将队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。

    图 5-4   队列的先入先出规则

    ","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 中,队列类 Qeque 是双向队列,也可作为队列使用\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 和遍历方法,可以把 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
    可视化运行

    全屏观看 >

    ","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   基于链表实现队列的入队出队操作

    以下是用链表实现队列的代码:

    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        # 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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    // 如果队列为空,则令头、尾节点都指向该节点\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            // 如果队列为空,则令头、尾节点都指向该节点\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    // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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    # 如果队列为空,则令头,尾节点都指向该节点\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)\\) ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。

    我们可以使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。

    基于此设计,数组中包含元素的有效区间为 [front, rear - 1],各种操作的实现方法如图 5-6 所示。

    • 入队操作:将输入元素赋值给 rear 索引处,并将 size 增加 1 。
    • 出队操作:只需将 front 增加 1 ,并将 size 减少 1 。

    可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 \\(O(1)\\) 。

    <1><2><3>

    图 5-6   基于数组实现队列的入队出队操作

    你可能会发现一个问题:在不断进行入队和出队的过程中,frontrear 都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。

    对于环形数组,我们需要让 frontrear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示:

    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  # 队首指针,指向队首元素\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        # 队首指针向后移动一位,若越过尾部,则返回到数组头部\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;       // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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; // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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;   // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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   // 队首指针,指向队首元素\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    // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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 // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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; // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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; // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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; // 队首指针,指向队首元素\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    // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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,        // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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;       // 队首指针,指向队首元素\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    // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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 // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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 # 队首指针,指向队首元素\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    # 队首指针向后移动一位,若越过尾部,则返回到数组头部\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 所示,我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫作“入栈”,删除栈顶元素的操作叫作“出栈”。

    图 5-1   栈的先入后出规则

    ","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
    可视化运行

    全屏观看 >

    ","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   栈的实现","text":"

    为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。

    栈遵循先入后出的原则,因此我们只能在栈顶添加或删除元素。然而,数组和链表都可以在任意位置添加和删除元素,因此栈可以视为一种受限制的数组或链表。换句话说,我们可以“屏蔽”数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性。

    ","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   基于链表的实现","text":"

    使用链表实现栈时,我们可以将链表的头节点视为栈顶,尾节点视为栈底。

    如图 5-2 所示,对于入栈操作,我们只需将元素插入链表头部,这种节点插入方法被称为“头插法”。而对于出栈操作,只需将头节点从链表中删除即可。

    <1><2><3>

    图 5-2   基于链表实现栈的入栈出栈操作

    以下是基于链表实现栈的示例代码:

    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 所示,入栈与出栈操作分别对应在数组尾部添加元素与删除元素,时间复杂度都为 \\(O(1)\\) 。

    <1><2><3>

    图 5-3   基于数组实现栈的入栈出栈操作

    由于入栈的元素可能会源源不断地增加,因此我们可以使用动态数组,这样就无须自行处理数组扩容问题。以下为示例代码:

    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":"

    支持操作

    两种实现都支持栈定义中的各项操作。数组实现额外支持随机访问,但这已超出了栈的定义范畴,因此一般不会用到。

    时间效率

    在基于数组的实现中,入栈和出栈操作都在预先分配好的连续内存中进行,具有很好的缓存本地性,因此效率较高。然而,如果入栈时超出数组容量,会触发扩容机制,导致该次入栈操作的时间复杂度变为 \\(O(n)\\) 。

    在基于链表的实现中,链表的扩容非常灵活,不存在上述数组扩容时效率降低的问题。但是,入栈操作需要初始化节点对象并修改指针,因此效率相对较低。不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率。

    综上所述,当入栈与出栈操作的元素是基本数据类型时,例如 intdouble ,我们可以得出以下结论。

    • 基于数组实现的栈在触发扩容时效率会降低,但由于扩容是低频操作,因此平均效率更高。
    • 基于链表实现的栈可以提供更加稳定的效率表现。

    空间效率

    在初始化列表时,系统会为列表分配“初始容量”,该容量可能超出实际需求;并且,扩容机制通常是按照特定倍率(例如 2 倍)进行扩容的,扩容后的容量也可能超出实际需求。因此,基于数组实现的栈可能造成一定的空间浪费。

    然而,由于链表节点需要额外存储指针,因此链表节点占用的空间相对较大。

    综上,我们不能简单地确定哪种实现更加节省内存,需要针对具体情况进行分析。

    ","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   栈的典型应用","text":"
    • 浏览器中的后退与前进、软件中的撤销与反撤销。每当我们打开新的网页,浏览器就会对上一个网页执行入栈,这样我们就可以通过后退操作回到上一个网页。后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么需要两个栈来配合实现。
    • 程序内存管理。每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会不断执行出栈操作。
    ","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":"
    • 栈是一种遵循先入后出原则的数据结构,可通过数组或链表来实现。
    • 在时间效率方面,栈的数组实现具有较高的平均效率,但在扩容过程中,单次入栈操作的时间复杂度会劣化至 \\(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:在出栈后,是否需要释放出栈节点的内存?

    如果后续仍需要使用弹出节点,则不需要释放内存。若之后不需要用到,JavaPython 等语言拥有自动垃圾回收机制,因此不需要手动释放内存;在 CC++ 中需要手动释放内存。

    Q:双向队列像是两个栈拼接在了一起,它的用途是什么?

    双向队列就像是栈和队列的组合或两个栈拼在了一起。它表现的是栈 + 队列的逻辑,因此可以实现栈与队列的所有应用,并且更加灵活。

    Q:撤销(undo)和反撤销(redo)具体是如何实现的?

    使用两个栈,栈 A 用于撤销,栈 B 用于反撤销。

    1. 每当用户执行一个操作,将这个操作压入栈 A ,并清空栈 B
    2. 当用户执行“撤销”时,从栈 A 中弹出最近的操作,并将其压入栈 B
    3. 当用户执行“反撤销”时,从栈 B 中弹出最近的操作,并将其压入栈 A
    ","path":["第 5 章   栈与队列","5.4   小结"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章   树","text":"

    Abstract

    参天大树充满生命力,根深叶茂,分枝扶疏。

    它为我们展现了数据分治的生动形态。

    ","path":["第 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 章   树"],"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 ,因此我们无法仅凭该序列来推测 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// 使用 int 的包装类 Integer ,就可以使用 null 来标记空位\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
    /* 二叉树的数组表示 */\n// 使用 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// 使用 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// 使用 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 年 G. M. Adelson-Velsky 和 E. M. 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 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种平衡二叉搜索树(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    # 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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  // 以 child 为原点,将 node 向右旋转\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            // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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  # 以 child 为原点,将 node 向右旋转\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_1","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    # 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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  // 以 child 为原点,将 node 向左旋转\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            // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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  # 以 child 为原点,将 node 向左旋转\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":"

    对于图 7-30 中的失衡节点 3 ,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先对 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-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_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_2","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_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.valnum 之间的大小关系。

    • 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 or 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        # 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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    // 用 tmp 覆盖 cur\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 or 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            // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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    # 用 tmp 覆盖 cur\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
    可视化运行

    全屏观看 >

    ","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# 在 n1 -> n2 中间插入节点 P\nn1.left = p\np.left = n2\n# 删除节点 P\nn1.left = n2\n
    binary_tree.cpp
    /* 插入与删除节点 */\nTreeNode* P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1->left = P;\nP->left = n2;\n// 删除节点 P\nn1->left = n2;\n// 释放内存\ndelete P;\n
    binary_tree.java
    TreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.cs
    /* 插入与删除节点 */\nTreeNode P = new(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.go
    /* 插入与删除节点 */\n// 在 n1 -> n2 中间插入节点 P\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// 删除节点 P\nn1.Left = n2\n
    binary_tree.swift
    let P = TreeNode(x: 0)\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P\nP.left = n2\n// 删除节点 P\nn1.left = n2\n
    binary_tree.js
    /* 插入与删除节点 */\nlet P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.ts
    /* 插入与删除节点 */\nconst P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.dart
    /* 插入与删除节点 */\nTreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.rs
    let p = TreeNode::new(0);\n// 在 n1 -> n2 中间插入节点 P\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// 在 n1 -> n2 中间插入节点 P\nn1->left = P;\nP->left = n2;\n// 删除节点 P\nn1->left = n2;\n// 释放内存\nfree(P);\n
    binary_tree.kt
    val P = TreeNode(0)\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P\nP.left = n2\n// 删除节点 P\nn1.left = n2\n
    binary_tree.rb
    # 插入与删除节点\n_p = TreeNode.new(0)\n# 在 n1 -> n2 中间插入节点 _p\nn1.left = _p\n_p.left = n2\n# 删除节点\nn1.left = n2\n
    可视化运行

    全屏观看 >

    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:右旋操作是处理失衡节点 nodechildgrand_child 之间的关系,那 node 的父节点和 node 原来的连接不需要维护吗?右旋操作后岂不是断掉了?

    我们需要从递归的视角来看这个问题。右旋操作 right_rotate(root) 传入的是子树的根节点,最终 return child 返回旋转之后的子树的根节点。子树的根节点和其父节点的连接是在该函数返回后完成的,不属于右旋操作的维护范围。

    Q:在 C++ 中,函数被划分到 privatepublic 中,这方面有什么考量吗?为什么要将 height() 函数和 updateHeight() 函数分别放在 publicprivate 中呢?

    主要看方法的使用范围,如果方法只在类内部使用,那么就设计为 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 章   附录"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"本章内容","text":"
    • 16.1   编程环境安装
    • 16.2   一起参与创作
    • 16.3   术语表
    ","path":["第 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. 点击“编辑图标”,如果遇到“需要 Fork 此仓库”的提示,请同意该操作。
    2. 修改 Markdown 源文件内容,检查内容的正确性,并尽量保持排版格式的统一。
    3. 在页面底部填写修改说明,然后点击“Propose file change”按钮。页面跳转后,点击“Create 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. 将本地所做更改 Commit ,然后 Push 至远程仓库。
    5. 刷新仓库网页,点击“Create 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":"

    推荐使用开源、轻量的 VS Code 作为本地集成开发环境(IDE)。访问 VS Code 官网,根据操作系统选择相应版本的 VS Code 进行下载和安装。

    图 16-1   从官网下载 VS Code

    VS Code 拥有强大的扩展包生态系统,支持大多数编程语言的运行和调试。以 Python 为例,安装“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 大 \\(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 AVL 树 red-black tree 红黑树 level-order traversal 层序遍历 breadth-first traversal 广度优先遍历 depth-first traversal 深度优先遍历 binary search tree 二叉搜索树 balanced binary search tree 平衡二叉搜索树 balance factor 平衡因子 heap 堆 max heap 大顶堆 min heap 小顶堆 priority queue 优先队列 heapify 堆化 top-\\(k\\) problem Top-\\(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 章   数组与链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"本章内容","text":"
    • 4.1   数组
    • 4.2   链表
    • 4.3   列表
    • 4.4   内存与缓存 *
    • 4.5   小结
    ","path":["第 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":"

    我们可以根据需求选用数组的两种初始化方式:无初始值、给定初始值。在未指定初始值的情况下,大多数编程语言会将数组元素初始化为 \\(0\\) :

    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
    可视化运行

    全屏观看 >

    ","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    \"\"\"在数组的索引 index 处插入元素 num\"\"\"\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 _num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    /* 在数组的索引 index 处插入元素 num */\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
    ### 在数组的索引 index 处插入元素 num ###\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// 请注意,JavaScript 的 Array 是动态数组,可以直接扩展\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// 请注意,TypeScript 的 Array 是动态数组,可以直接扩展\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# 请注意,Ruby 的 Array 是动态数组,可以直接扩展\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 中分别被记为 nullnullptrNone
    • 在 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
    可视化运行

    全屏观看 >

    数组整体是一个变量,比如数组 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 所示,假设我们想在相邻的两个节点 n0n1 之间插入一个新节点 P ,则只需改变两个节点引用(指针)即可,时间复杂度为 \\(O(1)\\) 。

    相比之下,在数组中插入元素的时间复杂度为 \\(O(n)\\) ,在大数据量下的效率较低。

    图 4-6   链表插入节点示例

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def insert(n0: ListNode, P: ListNode):\n    \"\"\"在链表的节点 n0 之后插入节点 P\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
    linked_list.cpp
    /* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.java
    /* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.cs
    /* 在链表的节点 n0 之后插入节点 P */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.go
    /* 在链表的节点 n0 之后插入节点 P */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
    linked_list.swift
    /* 在链表的节点 n0 之后插入节点 P */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
    linked_list.js
    /* 在链表的节点 n0 之后插入节点 P */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.ts
    /* 在链表的节点 n0 之后插入节点 P */\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
    /* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
    linked_list.rs
    /* 在链表的节点 n0 之后插入节点 P */\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
    /* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.kt
    /* 在链表的节点 n0 之后插入节点 P */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
    linked_list.rb
    ### 在链表的节点 n0 之后插入节点 _p ###\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-vs","level":2,"title":"4.2.2   数组 vs. 链表","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":"

    单向链表通常用于实现栈、队列、哈希表和图等数据结构。

    • 栈与队列:当插入和删除操作都在链表的一端进行时,它表现的特性为先进后出,对应栈;当插入操作在链表的一端进行,删除操作在链表的另一端进行,它表现的特性为先进先出,对应队列。
    • 哈希表:链式地址是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表中。
    • 图:邻接表是表示图的一种常用方式,其中图的每个顶点都与一个链表相关联,链表中的每个元素都代表与该顶点相连的其他顶点。

    双向链表常用于需要快速查找前一个和后一个元素的场景。

    • 高级数据结构:比如在红黑树、B 树中,我们需要访问节点的父节点,这可以通过在节点中保存一个指向父节点的引用来实现,类似于双向链表。
    • 浏览器历史:在网页浏览器中,当用户点击前进或后退按钮时,浏览器需要知道用户访问过的前一个和后一个网页。双向链表的特性使得这种操作变得简单。
    • LRU 算法:在缓存淘汰(LRU)算法中,我们需要快速找到最近最少使用的数据,以及支持快速添加和删除节点。这时候使用双向链表就非常合适。

    环形链表常用于需要周期性操作的场景,比如操作系统的资源调度。

    • 时间片轮转调度算法:在操作系统中,时间片轮转调度算法是一种常见的 CPU 调度算法,它需要对一组进程进行循环。每个进程被赋予一个时间片,当时间片用完时,CPU 将切换到下一个进程。这种循环操作可以通过环形链表来实现。
    • 数据缓冲区:在某些数据缓冲区的实现中,也可能会使用环形链表。比如在音频、视频播放器中,数据流可能会被分成多个缓冲块并放入一个环形链表,以便实现无缝播放。
    ","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   列表","text":"

    列表(list)是一个抽象的数据结构概念,它表示元素的有序集合,支持元素访问、修改、添加、删除和遍历等操作,无须使用者考虑容量限制的问题。列表可以基于链表或数组实现。

    • 链表天然可以看作一个列表,其支持元素增删查改操作,并且可以灵活动态扩容。
    • 数组也支持元素增删查改,但由于其长度不可变,因此只能看作一个具有长度限制的列表。

    当使用数组实现列表时,长度不可变的性质会导致列表的实用性降低。这是因为我们通常无法事先确定需要存储多少数据,从而难以选择合适的列表长度。若长度过小,则很可能无法满足使用需求;若长度过大,则会造成内存空间浪费。

    为解决此问题,我们可以使用动态数组(dynamic array)来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。

    实际上,许多编程语言中的标准库提供的列表是基于动态数组实现的,例如 Python 中的 list 、Java 中的 ArrayList 、C++ 中的 vector 和 C# 中的 List 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。

    ","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// 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[])\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
    可视化运行

    全屏观看 >

    ","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
    可视化运行

    全屏观看 >

    ","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)  # 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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:]...)...) // 在索引 3 处插入数字 6\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) // 在索引 3 处插入数字 6\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); // 在索引 3 处插入数字 6\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); // 在索引 3 处插入数字 6\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); // 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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);  // 在索引 3 处插入数字 6\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) # 在索引 3 处插入数字 6\n\n# 删除元素\nnums.delete_at(3) # 删除索引 3 处的元素\n
    可视化运行

    全屏观看 >

    ","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
    可视化运行

    全屏观看 >

    ","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
    可视化运行

    全屏观看 >

    ","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
    可视化运行

    全屏观看 >

    ","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 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。

    图 4-10   硬盘、内存和缓存之间的数据流通

    ","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:为什么数组要求相同类型的元素,而在链表中却没有强调相同类型呢?

    链表由节点组成,节点之间通过引用(指针)连接,各个节点可以存储不同类型的数据,例如 intdoublestringobject 等。

    相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,数组同时包含 intlong 两种类型,单个元素分别占用 4 字节和 8 字节 ,此时就不能用以下公式计算偏移量了,因为数组中包含了两种“元素长度”。

    # 元素内存地址 = 数组内存地址(首元素内存地址) + 元素长度 * 元素索引\n

    Q:删除节点 P 后,是否需要把 P.next 设为 None 呢?

    不修改 P.next 也可以。从该链表的角度看,从头节点遍历到尾节点已经不会遇到 P 了。这意味着节点 P 已经从链表中删除了,此时节点 P 指向哪里都不会对该链表产生影响。

    从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。

    Q:在链表中插入和删除操作的时间复杂度是 \\(O(1)\\) 。但是增删之前都需要 \\(O(n)\\) 的时间查找元素,那为什么时间复杂度不是 \\(O(n)\\) 呢?

    如果是先查找元素、再删除元素,时间复杂度确实是 \\(O(n)\\) 。然而,链表的 \\(O(1)\\) 增删的优势可以在其他应用上得到体现。例如,双向队列适合使用链表实现,我们维护一个指针变量始终指向头节点、尾节点,每次插入与删除操作都是 \\(O(1)\\) 。

    Q:图“链表定义与存储方式”中,浅蓝色的存储节点指针是占用一块内存地址吗?还是和节点值各占一半呢?

    该示意图只是定性表示,定量表示需要根据具体情况进行分析。

    • 不同类型的节点值占用的空间是不同的,比如 intlongdouble 和实例对象等。
    • 指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。

    Q:在列表末尾添加元素是否时时刻刻都为 \\(O(1)\\) ?

    如果添加元素时超出列表长度,则需要先扩容列表再添加。系统会申请一块新的内存,并将原列表的所有元素搬运过去,这时候时间复杂度就会是 \\(O(n)\\) 。

    Q:“列表的出现极大地提高了数组的实用性,但可能导致部分内存空间浪费”,这里的空间浪费是指额外增加的变量如容量、长度、扩容倍数所占的内存吗?

    这里的空间浪费主要有两方面含义:一方面,列表都会设定一个初始长度,我们不一定需要用这么多;另一方面,为了防止频繁扩容,扩容一般会乘以一个系数,比如 \\(\\times 1.5\\) 。这样一来,也会出现很多空位,我们通常不能完全填满它们。

    Q:在 Python 中初始化 n = [1, 2, 3] 后,这 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 的性能会更好。

    另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 stackqueue ,而非链表。

    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 章   回溯"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"本章内容","text":"
    • 13.1   回溯算法
    • 13.2   全排列问题
    • 13.3   子集和问题
    • 13.4   N 皇后问题
    • 13.5   小结
    ","path":["第 13 章   回溯"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   回溯算法","text":"

    回溯算法(backtracking algorithm)是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。

    回溯算法通常采用“深度优先搜索”来遍历解空间。在“二叉树”章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来,我们利用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。

    例题一

    给定一棵二叉树,搜索并记录所有值为 \\(7\\) 的节点,请返回节点列表。

    对于此题,我们前序遍历这棵树,并判断当前节点的值是否为 \\(7\\) ,若是,则将该节点的值加入结果列表 res 之中。相关过程实现如图 13-1 和以下代码所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序遍历:例题一\"\"\"\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    /* 前序遍历:例题一 */\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
    ### 前序遍历:例题一 ###\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":"

    之所以称之为回溯算法,是因为该算法在搜索解空间时会采用“尝试”与“回退”的策略。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。

    对于例题一,访问每个节点都代表一次“尝试”,而越过叶节点或返回父节点的 return 则表示“回退”。

    值得说明的是,回退并不仅仅包括函数返回。为解释这一点,我们对例题一稍作拓展。

    例题二

    在二叉树中搜索所有值为 \\(7\\) 的节点,请返回根节点到这些节点的路径。

    在例题一代码的基础上,我们需要借助一个列表 path 记录访问过的节点路径。当访问到值为 \\(7\\) 的节点时,则复制 path 并添加进结果列表 res 。遍历完成后,res 中保存的就是所有的解。代码如下所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序遍历:例题二\"\"\"\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    /* 前序遍历:例题二 */\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
    ### 前序遍历:例题二 ###\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":"

    复杂的回溯问题通常包含一个或多个约束条件,约束条件通常可用于“剪枝”。

    例题三

    在二叉树中搜索所有值为 \\(7\\) 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 \\(3\\) 的节点。

    为了满足以上约束条件,我们需要添加剪枝操作:在搜索过程中,若遇到值为 \\(3\\) 的节点,则提前返回,不再继续搜索。代码如下所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序遍历:例题三\"\"\"\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    /* 前序遍历:例题三 */\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
    ### 前序遍历:例题三 ###\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

    接下来,我们基于框架代码来解决例题三。状态 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    \"\"\"回溯算法:例题三\"\"\"\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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/* 回溯算法:例题三 */\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### 回溯算法:例题三 ###\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 的搜索过程对比

    相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰唆,但通用性更好。实际上,许多回溯问题可以在该框架下解决。我们只需根据具体问题来定义 statechoices ,并实现框架中的各个方法即可。

    ","path":["第 13 章   回溯","13.1   回溯算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   常用术语","text":"

    为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例,如表 13-1 所示。

    表 13-1   常见的回溯算法术语

    名词 定义 例题三 解(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 \\times n\\) 的棋盘上放置 \\(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":"

    为了满足列约束,我们可以利用一个长度为 \\(n\\) 的布尔型数组 cols 记录每一列是否有皇后。在每次决定放置前,我们通过 cols 将已有皇后的列进行剪枝,并在回溯中动态更新 cols 的状态。

    Tip

    请注意,矩阵的起点位于左上角,其中行索引从上到下增加,列索引从左到右增加。

    那么,如何处理对角线约束呢?设棋盘中某个格子的行列索引为 \\((row, col)\\) ,选定矩阵中的某条主对角线,我们发现该对角线上所有格子的行索引减列索引都相等,即主对角线上所有格子的 \\(row - col\\) 为恒定值。

    也就是说,如果两个格子满足 \\(row_1 - col_1 = row_2 - col_2\\) ,则它们一定处在同一条主对角线上。利用该规律,我们可以借助图 13-18 所示的数组 diags1 记录每条主对角线上是否有皇后。

    同理,次对角线上的所有格子的 \\(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\\) ,即数组 diags1diags2 的长度都为 \\(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)\\) 空间,数组 colsdiags1diags2 皆使用 \\(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\\) ,我们将第二个 \\(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":"

    请注意,虽然 selectedduplicated 都用于剪枝,但两者的目标不同。

    • 重复选择剪枝:整个搜索过程中只有一个 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; // 初始化解的数量为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 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    # 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 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  // 剪枝二:从 start 开始遍历,避免生成重复子集\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超过 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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超过 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  # 剪枝二:从 start 开始遍历,避免生成重复子集\n  for i in start...choices.length\n    # 剪枝一:若子集和超过 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    # 剪枝二:从 start 开始遍历,避免生成重复子集\n    # 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超过 target ,则直接结束循环\n        # 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0:\n            break\n        # 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0 {\n            break\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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  // 剪枝二:从 start 开始遍历,避免生成重复子集\n  // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超过 target ,则直接结束循环\n    // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超过 target ,则直接跳过\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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  # 剪枝二:从 start 开始遍历,避免生成重复子集\n  # 剪枝三:从 start 开始遍历,避免重复选择同一元素\n  for i in start...choices.length\n    # 剪枝一:若子集和超过 target ,则直接结束循环\n    # 这是因为数组已排序,后边元素更大,子集和一定超过 target\n    break if target - choices[i] < 0\n    # 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\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-q-a","level":3,"title":"2.   Q & A","text":"

    Q:怎么理解回溯和递归的关系?

    总的来看,回溯是一种“算法策略”,而递归更像是一个“工具”。

    • 回溯算法通常基于递归实现。然而,回溯是递归的应用场景之一,是递归在搜索问题中的应用。
    • 递归的结构体现了“子问题分解”的解题范式,常用于解决分治、回溯、动态规划(记忆化递归)等问题。
    ","path":["第 13 章   回溯","13.5   小结"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章   复杂度分析","text":"

    Abstract

    复杂度分析犹如浩瀚的算法宇宙中的时空向导。

    它带领我们在时间与空间这两个维度上深入探索,寻找更优雅的解决方案。

    ","path":["第 2 章   复杂度分析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"本章内容","text":"
    • 2.1   算法效率评估
    • 2.2   迭代与递归
    • 2.3   时间复杂度
    • 2.4   空间复杂度
    • 2.5   小结
    ","path":["第 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 循环是最常见的迭代形式之一,适合在预先知道迭代次数时使用。

    以下函数基于 for 循环实现了求和 \\(1 + 2 + \\dots + n\\) ,求和结果使用变量 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 循环中,程序每轮都会先检查条件,如果条件为真,则继续执行,否则就结束循环。

    下面我们用 while 循环来实现求和 \\(1 + 2 + \\dots + n\\) :

    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_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 所示,这样不断递归调用下去,最终将产生一棵层数为 \\(n\\) 的递归树(recursion tree)。

    图 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() 在循环中调用了 \\(n\\) 次 function() ,每轮中的 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_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_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 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":"

    算法效率评估

    • 时间效率和空间效率是衡量算法优劣的两个主要评价指标。
    • 我们可以通过实际测试来评估算法效率,但难以消除测试环境的影响,且会耗费大量计算资源。
    • 复杂度分析可以消除实际测试的弊端,分析结果适用于所有运行平台,并且能够揭示算法在不同数据规模下的效率。

    时间复杂度

    • 时间复杂度用于衡量算法运行时间随数据量增长的趋势,可以有效评估算法效率,但在某些情况下可能失效,如在输入的数据量较小或时间复杂度相同时,无法精确对比算法效率的优劣。
    • 最差时间复杂度使用大 \\(O\\) 符号表示,对应函数渐近上界,反映当 \\(n\\) 趋向正无穷时,操作数量 \\(T(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)与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。

    下面以几种常见的编程语言为例来说明。

    • C 语言是过程式编程语言,没有面向对象的概念,所以只有函数。但我们可以通过创建结构体(struct)来模拟面向对象编程,与结构体相关联的函数就相当于其他编程语言中的方法。
    • Java 和 C# 是面向对象的编程语言,代码块(方法)通常作为某个类的一部分。静态方法的行为类似于函数,因为它被绑定在类上,不能访问特定的实例变量。
    • C++ 和 Python 既支持过程式编程(函数),也支持面向对象编程(方法)。

    Q:图解“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?

    不是,该图展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。

    假设取 \\(n = 8\\) ,你可能会发现每条曲线的值与函数对应不上。这是因为每条曲线都包含一个常数项,用于将取值范围压缩到一个视觉舒适的范围内。

    在实际中,因为我们通常不知道每个方法的“常数项”复杂度是多少,所以一般无法仅凭复杂度来选择 \\(n = 8\\) 之下的最优解法。但对于 \\(n = 8^5\\) 就很好选了,这时增长趋势已经占主导了。

    Q 是否存在根据实际使用场景,选择牺牲时间(或空间)来设计算法的情况?

    在实际应用中,大部分情况会选择牺牲空间换时间。例如数据库索引,我们通常选择建立 B+ 树或哈希索引,占用大量内存空间,以换取 \\(O(\\log n)\\) 甚至 \\(O(1)\\) 的高效查询。

    在空间资源宝贵的场景,也会选择牺牲时间换空间。例如在嵌入式开发中,设备内存很宝贵,工程师可能会放弃使用哈希表,选择使用数组顺序查找,以节省内存占用,代价是查找变慢。

    ","path":["第 2 章   复杂度分析","2.5   小结"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   时间复杂度","text":"

    运行时间可以直观且准确地反映算法的效率。如果我们想准确预估一段代码的运行时间,应该如何操作呢?

    1. 确定运行平台,包括硬件配置、编程语言、系统环境等,这些因素都会影响代码的运行效率。
    2. 评估各种计算操作所需的运行时间,例如加法操作 + 需要 1 ns ,乘法操作 * 需要 10 ns ,打印操作 print() 需要 5 ns 等。
    3. 统计代码中所有的计算操作,并将所有操作的执行时间求和,从而得到运行时间。

    例如在以下代码中,输入数据大小为 \\(n\\) :

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # 在某运行平台下\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # 循环 n 次\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
    // 在某运行平台下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
    // 在某运行平台下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
    // 在某运行平台下\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
    // 在某运行平台下\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // 循环 n 次\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
    // 在某运行平台下\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 循环 n 次\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
    // 在某运行平台下\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 循环 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // 在某运行平台下\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 循环 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // 在某运行平台下\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // 循环 n 次\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
    // 在某运行平台下\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // 循环 n 次\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
    // 在某运行平台下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
    // 在某运行平台下\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 循环 n 次\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
    # 在某运行平台下\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # 循环 n 次\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

    根据以上方法,可以得到算法的运行时间为 \\((6n + 12)\\) ns :

    \\[ 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\\) ,给定三个算法 ABC

    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 只有 \\(1\\) 个打印操作,算法运行时间不随着 \\(n\\) 增大而增长。我们称此算法的时间复杂度为“常数阶”。
    • 算法 B 中的打印操作需要循环 \\(n\\) 次,算法运行时间随着 \\(n\\) 增大呈线性增长。此算法的时间复杂度被称为“线性阶”。
    • 算法 C 中的打印操作需要循环 \\(1000000\\) 次,虽然运行时间很长,但它与输入数据大小 \\(n\\) 无关。因此 C 的时间复杂度和 A 相同,仍为“常数阶”。

    图 2-7   算法 A、B 和 C 的时间增长趋势

    相较于直接统计算法的运行时间,时间复杂度分析有哪些特点呢?

    • 时间复杂度能够有效评估算法效率。例如,算法 B 的运行时间呈线性增长,在 \\(n > 1\\) 时比算法 A 更慢,在 \\(n > 1000000\\) 时比算法 C 更慢。事实上,只要输入数据大小 \\(n\\) 足够大,复杂度为“常数阶”的算法一定优于“线性阶”的算法,这正是时间增长趋势的含义。
    • 时间复杂度的推算方法更简便。显然,运行平台和计算操作类型都与算法运行时间的增长趋势无关。因此在时间复杂度分析中,我们可以简单地将所有计算操作的执行时间视为相同的“单位时间”,从而将“计算操作运行时间统计”简化为“计算操作数量统计”,这样一来估算难度就大大降低了。
    • 时间复杂度也存在一定的局限性。例如,尽管算法 AC 的时间复杂度相同,但实际运行时间差别很大。同样,尽管算法 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)\\) ,这个数学符号称为大 \\(O\\) 记号(big-\\(O\\) notation),表示函数 \\(T(n)\\) 的渐近上界(asymptotic upper bound)。

    时间复杂度分析本质上是计算“操作数量 \\(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","level":3,"title":"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","level":3,"title":"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 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\\) 为输入数据大小;在第二个示例中,数组长度 \\(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 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":"

    生物学的“细胞分裂”是指数阶增长的典型例子:初始状态为 \\(1\\) 个细胞,分裂一轮后变为 \\(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 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 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 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 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    # 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 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  // 从 1 个分裂出 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    // 从 1 个分裂出 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    // 从 1 个分裂出 n 个\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
    time_complexity.rb
    ### 阶乘阶(递归实现)###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # 从 1 个分裂出 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":"

    算法的时间效率往往不是固定的,而是与输入数据的分布有关。假设输入一个长度为 \\(n\\) 的数组 nums ,其中 nums 由从 \\(1\\) 至 \\(n\\) 的数字组成,每个数字只出现一次;但元素顺序是随机打乱的,任务目标是返回元素 \\(1\\) 的索引。我们可以得出以下结论。

    • nums = [?, ?, ..., 1] ,即当末尾元素是 \\(1\\) 时,需要完整遍历数组,达到最差时间复杂度 \\(O(n)\\) 。
    • nums = [1, ?, ?, ...] ,即当首个元素为 \\(1\\) 时,无论数组多长都不需要继续遍历,达到最佳时间复杂度 \\(\\Omega(1)\\) 。

    “最差时间复杂度”对应函数渐近上界,使用大 \\(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    \"\"\"查找数组 nums 中数字 1 所在索引\"\"\"\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    // 使用系统时间生成随机种子\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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/* 查找数组 nums 中数字 1 所在索引 */\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### 查找数组 nums 中数字 1 所在索引 ###\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 章   数据结构"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"本章内容","text":"
    • 3.1   数据结构分类
    • 3.2   基本数据类型
    • 3.3   数字编码 *
    • 3.4   字符编码 *
    • 3.5   小结
    ","path":["第 3 章   数据结构"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   基本数据类型","text":"

    当谈及计算机中的数据时,我们会想到文本、图片、视频、语音、3D 模型等各种形式。尽管这些数据的组织形式各异,但它们都由各种基本数据类型构成。

    基本数据类型是 CPU 可以直接进行运算的类型,在算法中直接被使用,主要包括以下几种。

    • 整数类型 byteshortintlong
    • 浮点数类型 floatdouble ,用于表示小数。
    • 字符类型 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 类型,单个字符实际上是长度为 1 的字符串 str
    • C 和 C++ 未明确规定基本数据类型的大小,而因实现和平台各异。表 3-1 遵循 LP64 数据模型,其用于包括 Linux 和 macOS 在内的 Unix 64 位操作系统。
    • 字符 char 的大小在 C 和 C++ 中为 1 字节,在大多数编程语言中取决于特定的字符编码方法,详见“字符编码”章节。
    • 即使表示布尔量仅需 1 位(\\(0\\) 或 \\(1\\)),它在内存中通常也存储为 1 字节。这是因为现代计算机 CPU 通常将 1 字节作为最小寻址内存单元。

    那么,基本数据类型与数据结构之间有什么联系呢?我们知道,数据结构是在计算机中组织与存储数据的方式。这句话的主语是“结构”而非“数据”。

    如果想表示“一排数字”,我们自然会想到使用数组。这是因为数组的线性结构可以表示数字的相邻关系和顺序关系,但至于存储的内容是整数 int、小数 float 还是字符 char ,则与“数据结构”无关。

    换句话说,基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”。例如以下代码,我们用相同的数据结构(数组)来存储与表示不同的基本数据类型,包括 intfloatcharbool 等。

    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
    可视化运行

    全屏观看 >

    ","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 字符集。它在 ASCII 的 7 位基础上扩展到 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 的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截至 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号甚至表情符号等。

    Unicode 作为一种通用字符集,本质上是给每个字符分配唯一的“码点”(字符编号),其取值范围为 U+0000 至 U+10FFFF,构成了统一的字符编号空间。然而,Unicode 并没有规定在计算机中如何存储这些字符码点。我们不禁会问:当多种长度的 Unicode 码点同时出现在一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符?

    对于以上问题,一种直接的解决方案是将所有字符存储为等长的编码。如图 3-7 所示,“Hello”中的每个字符占用 1 字节,“算法”中的每个字符占用 2 字节。我们可以通过高位填 0 将“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 字符在 Unicode 字符集中占据了前 128 个码点。也就是说,UTF-8 编码可以向下兼容 ASCII 码。这意味着我们可以使用 UTF-8 来解析年代久远的 ASCII 码文本。
    • 对于长度为 \\(n\\) 字节的字符(其中 \\(n > 1\\)),将首个字节的高 \\(n\\) 位都设置为 \\(1\\) ,第 \\(n + 1\\) 位设置为 \\(0\\) ;从第二个字节开始,将每个字节的高 2 位都设置为 \\(10\\) ;其余所有位用于填充字符的 Unicode 码点。

    图 3-8 展示了“Hello算法”对应的 UTF-8 编码。观察发现,由于最高 \\(n\\) 位都设置为 \\(1\\) ,因此系统可以通过读取最高位 \\(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\\) 个字符,我们需要从字符串的开始处遍历到第 \\(i\\) 个字符,这需要 \\(O(n)\\) 的时间。
    • 字符计数:与随机访问类似,计算 UTF-16 编码的字符串的长度也是 \\(O(1)\\) 的操作。但是,计算 UTF-8 编码的字符串的长度需要遍历整个字符串。
    • 字符串操作:在 UTF-16 编码的字符串上,很多字符串操作(如分割、连接、插入、删除等)更容易进行。在 UTF-8 编码的字符串上,进行这些操作通常需要额外的计算,以确保不会产生无效的 UTF-8 编码。

    实际上,编程语言的字符编码方案设计是一个很有趣的话题,涉及许多因素。

    • Java 的 String 类型使用 UTF-16 编码,每个字符占用 2 字节。这是因为 Java 语言设计之初,人们认为 16 位足以表示所有可能的字符。然而,这是一个不正确的判断。后来 Unicode 规范扩展到了超过 16 位,所以 Java 中的字符现在可能由一对 16 位的值(称为“代理对”)表示。
    • JavaScript 和 TypeScript 的字符串使用 UTF-16 编码的原因与 Java 类似。当 1995 年 Netscape 公司首次推出 JavaScript 语言时,Unicode 还处于发展早期,那时候使用 16 位的编码就足以表示所有的 Unicode 字符了。
    • C# 使用 UTF-16 编码,主要是因为 .NET 平台是由 Microsoft 设计的,而 Microsoft 的很多技术(包括 Windows 操作系统)都广泛使用 UTF-16 编码。

    由于以上编程语言对字符数量的低估,它们不得不采取“代理对”的方式来表示超过 16 位长度的 Unicode 字符。这是一个不得已为之的无奈之举。一方面,包含代理对的字符串中,一个字符可能占用 2 字节或 4 字节,从而丧失了等长编码的优势。另一方面,处理代理对需要额外增加代码,这提高了编程的复杂性和调试难度。

    出于以上原因,部分编程语言提出了一些不同的编码方案。

    • Python 中的 str 使用 Unicode 编码,并采用一种灵活的字符串表示,存储的字符长度取决于字符串中最大的 Unicode 码点。若字符串中全部是 ASCII 字符,则每个字符占用 1 字节;如果有字符超出了 ASCII 范围,但全部在基本多语言平面(BMP)内,则每个字符占用 2 字节;如果有超出 BMP 的字符,则每个字符占用 4 字节。
    • Go 语言的 string 类型在内部使用 UTF-8 编码。Go 语言还提供了 rune 类型,它用于表示单个 Unicode 码点。
    • Rust 语言的 strString 类型在内部使用 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 位的 \\(1\\) 会被舍弃。也就是说,负零的补码为 \\(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":"

    细心的你可能会发现:intfloat 长度相同,都是 4 字节 ,但为什么 float 的取值范围远大于 int ?这非常反直觉,因为按理说 float 需要表示小数,取值范围应该变小才对。

    实际上,这是因为浮点数 float 采用了不同的表示方式。记一个 32 比特长度的二进制数为:

    \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

    根据 IEEE 754 标准,32-bit 长度的 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   IEEE 754 标准下的 float 的计算示例

    观察图 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":"
    • 数据结构可以从逻辑结构和物理结构两个角度进行分类。逻辑结构描述了数据元素之间的逻辑关系,而物理结构描述了数据在计算机内存中的存储方式。
    • 常见的逻辑结构包括线性、树状和网状等。通常我们根据逻辑结构将数据结构分为线性(数组、链表、栈、队列)和非线性(树、图、堆)两种。哈希表的实现可能同时包含线性数据结构和非线性数据结构。
    • 当程序运行时,数据被存储在计算机内存中。每个内存空间都拥有对应的内存地址,程序通过这些内存地址访问数据。
    • 物理结构主要分为连续空间存储(数组)和分散空间存储(链表)。所有数据结构都是由数组、链表或两者的组合实现的。
    • 计算机中的基本数据类型包括整数 byteshortintlong ,浮点数 floatdouble ,字符 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:基于数组实现的数据结构也称“静态数据结构” 是否有歧义?栈也可以进行出栈和入栈等操作,这些操作都是“动态”的。

    栈确实可以实现动态的数据操作,但数据结构仍然是“静态”(长度不可变)的。尽管基于数组的数据结构可以动态地添加或删除元素,但它们的容量是固定的。如果数据量超出了预分配的大小,就需要创建一个新的更大的数组,并将旧数组的内容复制到新数组中。

    Q:在构建栈(队列)的时候,未指定它的大小,为什么它们是“静态数据结构”呢?

    在高级编程语言中,我们无须人工指定栈(队列)的初始容量,这个工作由类内部自动完成。例如,Java 的 ArrayList 的初始容量通常为 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 章   分治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"本章内容","text":"
    • 12.1   分治算法
    • 12.2   分治搜索策略
    • 12.3   构建树问题
    • 12.4   汉诺塔问题
    • 12.5   小结
    ","path":["第 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

    给定一个长度为 \\(n\\) 的有序数组 nums ,其中所有元素都是唯一的,请查找元素 target

    从分治角度,我们将搜索区间 \\([i, j]\\) 对应的子问题记为 \\(f(i, j)\\) 。

    以原问题 \\(f(0, n-1)\\) 为起始点,通过以下步骤进行二分查找。

    1. 计算搜索区间 \\([i, j]\\) 的中点 \\(m\\) ,根据它排除一半搜索区间。
    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":"

    原问题定义为从 preorderinorder 构建二叉树,是一个典型的分治问题。

    • 问题可以分解:从分治的角度切入,我们可以将原问题划分为两个子问题:构建左子树、构建右子树,加上一步操作:初始化根节点。而对于每棵子树(子问题),我们仍然可以复用以上划分方法,将其划分为更小的子树(子问题),直至达到最小子问题(空子树)时终止。
    • 子问题是独立的:左子树和右子树是相互独立的,它们之间没有交集。在构建左子树时,我们只需关注中序遍历和前序遍历中与左子树对应的部分。右子树同理。
    • 子问题的解可以合并:一旦得到了左子树和右子树(子问题的解),我们就可以将它们链接到根节点上,得到原问题的解。
    ","path":["第 12 章   分治","12.3   构建二叉树问题"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   如何划分子树","text":"

    根据以上分析,这道题可以使用分治来求解,但如何通过前序遍历 preorder 和中序遍历 inorder 来划分左子树和右子树呢?

    根据定义,preorderinorder 都可以划分为三个部分。

    • 前序遍历:[ 根节点 | 左子树 | 右子树 ] ,例如图 12-5 的树对应 [ 3 | 9 | 2 1 7 ]
    • 中序遍历:[ 左子树 | 根节点 | 右子树 ] ,例如图 12-5 的树对应 [ 9 | 3 | 1 2 7 ]

    以上图数据为例,我们可以通过图 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":"

    根据以上划分方法,我们已经得到根节点、左子树、右子树在 preorderinorder 中的索引区间。而为了描述这些索引区间,我们需要借助几个指针变量。

    • 将当前树的根节点在 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":"

    一方面,分治可以用来解决许多经典算法问题。

    • 寻找最近点对:该算法首先将点集分成两部分,然后分别找出两部分中的最近点对,最后找出跨越两部分的最近点对。
    • 大整数乘法:例如 Karatsuba 算法,它将大整数乘法分解为几个较小的整数的乘法和加法。
    • 矩阵乘法:例如 Strassen 算法,它将大矩阵乘法分解为多个小矩阵的乘法和加法。
    • 汉诺塔问题:汉诺塔问题可以通过递归解决,这是典型的分治策略应用。
    • 求解逆序对:在一个序列中,如果前面的数字大于后面的数字,那么这两个数字构成一个逆序对。求解逆序对问题可以利用分治的思想,借助归并排序进行求解。

    另一方面,分治在算法和数据结构的设计中应用得非常广泛。

    • 二分查找:二分查找是将有序数组从中点索引处分为两部分,然后根据目标值与中间元素值比较结果,决定排除哪一半区间,并在剩余区间执行相同的二分操作。
    • 归并排序:本节开头已介绍,不再赘述。
    • 快速排序:快速排序是选取一个基准值,然后把数组分为两个子数组,一个子数组的元素比基准值小,另一子数组的元素比基准值大,再对这两部分进行相同的划分操作,直至子数组只剩下一个元素。
    • 桶排序:桶排序的基本思想是将数据分散到多个桶,然后对每个桶内的元素进行排序,最后将各个桶的元素依次取出,从而得到一个有序数组。
    • 树:例如二叉搜索树、AVL 树、红黑树、B 树、B+ 树等,它们的查找、插入和删除等操作都可以视为分治策略的应用。
    • 堆:堆是一种特殊的完全二叉树,其各种操作,如插入、删除和堆化,实际上都隐含了分治的思想。
    • 哈希表:虽然哈希表并不直接应用分治,但某些哈希冲突解决方案间接应用了分治策略,例如,链式地址中的长链表会被转化为红黑树,以提升查询效率。

    可以看出,分治是一种“润物细无声”的算法思想,隐含在各种算法与数据结构之中。

    ","path":["第 12 章   分治","12.1   分治算法"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   汉诺塔问题","text":"

    在归并排序和构建二叉树中,我们都是将原问题分解为两个规模为原问题一半的子问题。然而对于汉诺塔问题,我们采用不同的分解策略。

    Question

    给定三根柱子,记为 ABC 。起始状态下,柱子 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)\\) 的过程可总结为:将两个圆盘借助 BA 移至 C 。其中,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 中剩余的一个圆盘从 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\\) 个圆盘借助 CA 移至 B
    2. 将剩余 \\(1\\) 个圆盘从 A 直接移至 C
    3. 将 \\(n-1\\) 个圆盘借助 AB 移至 C

    对于这两个子问题 \\(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) ,它的作用是将柱 src 顶部的 \\(i\\) 个圆盘借助缓冲柱 buf 移动至目标柱 tar

    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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    # 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar)\n    # 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    # 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    DFS(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    Move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfsHanota(i-1, src, tar, buf)\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar)\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* 求解汉诺塔问题 */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\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) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 src 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, src, tar);\n}\n\n/* 求解汉诺塔问题 */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n  dfs(i - 1, src, tar, buf);\n  // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n  move(src, tar);\n  // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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  // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move_pan(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\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) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar)\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\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    // 将 A 顶部 n 个圆盘借助 B 移到 C\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) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n  dfs(i - 1, src, tar, buf)\n  # 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n  move(src, tar)\n  # 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n  dfs(i - 1, buf, src, tar)\nend\n\n### 求解汉诺塔问题 ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # 将 A 顶部 n 个圆盘借助 B 移到 C\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}\\) 秒,合约 \\(5850\\) 亿年,远远超过了现在对宇宙年龄的估计。所以,倘若这个传说是真的,我们应该不需要担心世界末日的到来。

    ","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 章   动态规划"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"本章内容","text":"
    • 14.1   初探动态规划
    • 14.2   DP 问题特性
    • 14.3   DP 解题思路
    • 14.4   0-1 背包问题
    • 14.5   完全背包问题
    • 14.6   编辑距离问题
    • 14.7   小结
    ","path":["第 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\\) ,则从地面爬到第 \\(3\\) 阶的最小代价为 \\(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 所示,爬上第 \\(3\\) 阶仅剩 \\(2\\) 种可行方案,其中连续三次跳 \\(1\\) 阶的方案不满足约束条件,因此被舍弃。

    图 14-8   带约束爬到第 3 阶的方案数量

    在该问题中,如果上一轮是跳 \\(1\\) 阶上来的,那么下一轮就必须跳 \\(2\\) 阶。这意味着,下一步选择不能由当前状态(当前所在楼梯阶数)独立决定,还和前一个状态(上一轮所在楼梯阶数)有关。

    不难发现,此问题已不满足无后效性,状态转移方程 \\(dp[i] = dp[i-1] + dp[i-2]\\) 也失效了,因为 \\(dp[i-1]\\) 代表本轮跳 \\(1\\) 阶,但其中包含了许多“上一轮是跳 \\(1\\) 阶上来的”方案,而为了满足约束,我们就不能将 \\(dp[i-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

    给定一个 \\(n \\times m\\) 的二维网格 grid ,网格中的每个单元格包含一个非负整数,表示该单元格的代价。机器人以左上角单元格为起始点,每次只能向下或者向右移动一步,直至到达右下角单元格。请返回从左上角到右下角的最小路径和。

    图 14-10 展示了一个例子,给定网格的最小路径和为 \\(13\\) 。

    图 14-10   最小路径和示例数据

    第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

    本题的每一轮的决策就是从当前格子向下或向右走一步。设当前格子的行列索引为 \\([i, j]\\) ,则向下或向右走一步后,索引变为 \\([i+1, j]\\) 或 \\([i, j+1]\\) 。因此,状态应包含行索引和列索引两个变量,记为 \\([i, j]\\) 。

    状态 \\([i, j]\\) 对应的子问题为:从起始点 \\([0, 0]\\) 走到 \\([i, j]\\) 的最小路径和,解记为 \\(dp[i, j]\\) 。

    至此,我们就得到了图 14-11 所示的二维 \\(dp\\) 矩阵,其尺寸与输入网格 \\(grid\\) 相同。

    图 14-11   状态定义与 dp 表

    Note

    动态规划和回溯过程可以描述为一个决策序列,而状态由所有决策变量构成。它应当包含描述解题进度的所有变量,其包含了足够的信息,能够用来推导出下一个状态。

    每个状态都对应一个子问题,我们会定义一个 \\(dp\\) 表来存储所有子问题的解,状态的每个独立变量都是 \\(dp\\) 表的一个维度。从本质上看,\\(dp\\) 表是状态和子问题的解之间的映射。

    第二步:找出最优子结构,进而推导出状态转移方程

    对于状态 \\([i, j]\\) ,它只能从上边格子 \\([i-1, j]\\) 和左边格子 \\([i, j-1]\\) 转移而来。因此最优子结构为:到达 \\([i, j]\\) 的最小路径和由 \\([i, j-1]\\) 的最小路径和与 \\([i-1, j]\\) 的最小路径和中较小的那一个决定。

    根据以上分析,可推出图 14-12 所示的状态转移方程:

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

    图 14-12   最优子结构与状态转移方程

    Note

    根据定义好的 \\(dp\\) 表,思考原问题和子问题的关系,找出通过子问题的最优解来构造原问题的最优解的方法,即最优子结构。

    一旦我们找到了最优子结构,就可以使用它来构建出状态转移方程。

    第三步:确定边界条件和状态转移顺序

    在本题中,处在首行的状态只能从其左边的状态得来,处在首列的状态只能从其上边的状态得来,因此首行 \\(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","level":3,"title":"1.   方法一:暴力搜索","text":"

    从状态 \\([i, j]\\) 开始搜索,不断分解为更小的状态 \\([i-1, j]\\) 和 \\([i, j-1]\\) ,递归函数包括以下要素。

    • 递归参数:状态 \\([i, j]\\) 。
    • 返回值:从 \\([0, 0]\\) 到 \\([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","level":3,"title":"2.   方法二:记忆化搜索","text":"

    我们引入一个和网格 grid 相同尺寸的记忆列表 mem ,用于记录各个子问题的解,并将重叠子问题进行剪枝:

    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","level":3,"title":"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":"

    编辑距离,也称 Levenshtein 距离,指两个字符串之间互相转换的最少修改次数,通常用于在信息检索和自然语言处理中度量两个序列的相似度。

    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":"

    第一步:思考每轮的决策,定义状态,从而得到 \\(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, j]\\) 对应的子问题:将 \\(s\\) 的前 \\(i\\) 个字符更改为 \\(t\\) 的前 \\(j\\) 个字符所需的最少编辑步数。

    至此,得到一个尺寸为 \\((i+1) \\times (j+1)\\) 的二维 \\(dp\\) 表。

    第二步:找出最优子结构,进而推导出状态转移方程

    考虑子问题 \\(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] \\]

    第三步:确定边界条件和状态转移顺序

    当两字符串都为空时,编辑步数为 \\(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-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","level":2,"title":"14.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 -1\\) 阶或第 \\(i - 2\\) 阶迈向第 \\(i\\) 阶。

    由此便可得出一个重要推论:爬到第 \\(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","level":2,"title":"14.1.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","level":2,"title":"14.1.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\\) 对应子问题的解。
    • 将最小子问题对应的状态(第 \\(1\\) 阶和第 \\(2\\) 阶楼梯)称为初始状态。
    • 将递推公式 \\(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\\) 轮决策组成的过程,对于每个物体都有不放入和放入两种决策,因此该问题满足决策树模型。

    该问题的目标是求解“在限定背包容量下能放入物品的最大价值”,因此较大概率是一个动态规划问题。

    第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

    对于每个物品来说,不放入背包,背包容量不变;放入背包,背包容量减小。由此可得状态定义:当前物品编号 \\(i\\) 和背包容量 \\(c\\) ,记为 \\([i, c]\\) 。

    状态 \\([i, c]\\) 对应的子问题为:前 \\(i\\) 个物品在容量为 \\(c\\) 的背包中的最大价值,记为 \\(dp[i, c]\\) 。

    待求解的是 \\(dp[n, cap]\\) ,因此需要一个尺寸为 \\((n+1) \\times (cap+1)\\) 的二维 \\(dp\\) 表。

    第二步:找出最优子结构,进而推导出状态转移方程

    当我们做出物品 \\(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\\) ,则只能选择不放入背包。

    第三步:确定边界条件和状态转移顺序

    当无物品或背包容量为 \\(0\\) 时最大价值为 \\(0\\) ,即首列 \\(dp[i, 0]\\) 和首行 \\(dp[0, c]\\) 都等于 \\(0\\) 。

    当前状态 \\([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","level":3,"title":"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","level":3,"title":"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","level":3,"title":"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 的第一维 \\(i\\) 直接删除,并且把内循环更改为倒序遍历即可:

    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()\\) 改为求和运算符。

    编辑距离问题

    • 编辑距离(Levenshtein 距离)用于衡量两个字符串之间的相似度,其定义为从一个字符串到另一个字符串的最少编辑步数,编辑操作包括添加、删除、替换。
    • 编辑距离问题的状态定义为将 \\(s\\) 的前 \\(i\\) 个字符更改为 \\(t\\) 的前 \\(j\\) 个字符所需的最少编辑步数。当 \\(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":"

    对比两道题目的代码,状态转移中有一处从 \\(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":"

    零钱兑换可以看作完全背包问题的一种特殊情况,两者具有以下联系与不同点。

    • 两道题可以相互转换,“物品”对应“硬币”、“物品重量”对应“硬币面值”、“背包容量”对应“目标金额”。
    • 优化目标相反,完全背包问题是要最大化物品价值,零钱兑换问题是要最小化硬币数量。
    • 完全背包问题是求“不超过”背包容量下的解,零钱兑换是求“恰好”凑到目标金额的解。

    第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

    状态 \\([i, a]\\) 对应的子问题为:前 \\(i\\) 种硬币能够凑出金额 \\(a\\) 的最少硬币数量,记为 \\(dp[i, a]\\) 。

    二维 \\(dp\\) 表的尺寸为 \\((n+1) \\times (amt+1)\\) 。

    第二步:找出最优子结构,进而推导出状态转移方程

    本题与完全背包问题的状态转移方程存在以下两点差异。

    • 本题要求最小值,因此需将运算符 \\(\\max()\\) 更改为 \\(\\min()\\) 。
    • 优化主体是硬币数量而非商品价值,因此在选中硬币时执行 \\(+1\\) 即可。
    \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

    第三步:确定边界条件和状态转移顺序

    当目标金额为 \\(0\\) 时,凑出它的最少硬币数量为 \\(0\\) ,即首列所有 \\(dp[i, 0]\\) 都等于 \\(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]\\) 都初始化为 \\(1\\) 。当无硬币时,无法凑出任何 \\(>0\\) 的目标金额,因此首行所有 \\(dp[0, a]\\) 都等于 \\(0\\) 。

    ","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 章   图"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"本章内容","text":"
    • 9.1   图
    • 9.2   图基础操作
    • 9.3   图的遍历
    • 9.4   小结
    ","path":["第 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 所示。

    • 在无向图中,边表示两顶点之间的“双向”连接关系,例如微信或 QQ 中的“好友关系”。
    • 在有向图中,边具有方向性,即 \\(A \\rightarrow B\\) 和 \\(A \\leftarrow B\\) 两个方向的边是相互独立的,例如微博或抖音上的“关注”与“被关注”关系。

    图 9-2   有向图与无向图

    根据所有顶点是否连通,可分为连通图(connected graph)和非连通图(disconnected graph),如图 9-3 所示。

    • 对于连通图,从某个顶点出发,可以到达其余任意顶点。
    • 对于非连通图,从某个顶点出发,至少有一个顶点无法到达。

    图 9-3   连通图与非连通图

    我们还可以为边添加“权重”变量,从而得到如图 9-4 所示的有权图(weighted graph)。例如在《王者荣耀》等手游中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以用有权图来表示。

    图 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\\) 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 \\(1\\) 或 \\(0\\) 表示两个顶点之间是否存在边。

    如图 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)\\) 时间。而由于是无向图,因此需要同时更新两个方向的边。
    • 添加顶点:在邻接矩阵的尾部添加一行一列,并全部填 \\(0\\) 即可,使用 \\(O(n)\\) 时间。
    • 删除顶点:在邻接矩阵中删除一行一列。当删除首行首列时达到最差情况,需要将 \\((n-1)^2\\) 个元素“向左上移动”,从而使用 \\(O(n^2)\\) 时间。
    • 初始化:传入 \\(n\\) 个顶点,初始化长度为 \\(n\\) 的顶点列表 vertices ,使用 \\(O(n)\\) 时间;初始化 \\(n \\times n\\) 大小的邻接矩阵 adjMat ,使用 \\(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\\) 的顶点,则需遍历整个邻接表,将所有大于 \\(i\\) 的索引全部减 \\(1\\) ,效率很低。而如果每个顶点都是唯一的 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 的哈希表,它可以在 \\(O(1)\\) 时间复杂度下进行 key 的增删查改操作。根据 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 操作会返回新的的引用,必须让原引用重新赋值为新slice的引用\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   图的深度优先遍历步骤

    深度优先遍历的序列是否唯一?

    与广度优先遍历类似,深度优先遍历序列的顺序也不是唯一的。给定某顶点,先往哪个方向探索都可以,即邻接顶点的顺序可以任意打乱,都是深度优先遍历。

    以树的遍历为例,“根 \\(\\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":"
    • 图由顶点和边组成,可以表示为一组顶点和一组边构成的集合。
    • 相较于线性关系(链表)和分治关系(树),网络关系(图)具有更高的自由度,因而更为复杂。
    • 有向图的边具有方向性,连通图中的任意顶点均可达,有权图的每条边都包含权重变量。
    • 邻接矩阵利用矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 \\(1\\) 或 \\(0\\) 表示两个顶点之间有边或无边。邻接矩阵在增删查改操作上效率很高,但空间占用较多。
    • 邻接表使用多个链表来表示图,第 \\(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 章   贪心"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"本章内容","text":"
    • 15.1   贪心算法
    • 15.2   分数背包问题
    • 15.3   最大容量问题
    • 15.4   最大切分乘积问题
    • 15.5   小结
    ","path":["第 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(\\log n)\\) ,空间复杂度通常为 \\(O(\\log n)\\) 或 \\(O(n)\\) ,取决于编程语言的具体实现。

    除排序之外,在最差情况下,需要遍历整个物品列表,因此时间复杂度为 \\(O(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
    可视化运行

    全屏观看 >

    你可能会不由地发出感叹:So clean !贪心算法仅用约十行代码就解决了零钱兑换问题。

    ","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":"

    贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下列举了一些典型的贪心算法问题。

    • 硬币找零问题:在某些硬币组合下,贪心算法总是可以得到最优解。
    • 区间调度问题:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。
    • 分数背包问题:给定一组物品和一个载重量,你的目标是选择一组物品,使得总重量不超过载重量,且总价值最大。如果每次都选择性价比最高(价值 / 重量)的物品,那么贪心算法在一些情况下可以得到最优解。
    • 股票买卖问题:给定一组股票的历史价格,你可以进行多次买卖,但如果你已经持有股票,那么在卖出之前不能再买,目标是获取最大利润。
    • 霍夫曼编码:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最低的两个节点合并,最后得到的霍夫曼树的带权路径长度(编码长度)最小。
    • Dijkstra 算法:它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法。
    ","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\\) 的整数都应该被切分。

    贪心策略一:如果切分方案中包含 \\(\\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\\) 总是可以替换为两个 \\(3\\) ,从而获得更大的乘积。

    图 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 所示,我们无须通过循环来切分整数,而可以利用向下整除运算得到 \\(3\\) 的个数 \\(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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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 为 3 的个数,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() 内部调用 C 语言库的 pow() 函数,其执行浮点取幂,时间复杂度为 \\(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\\) :假设最优切分方案中存在 \\(\\geq 4\\) 的因子 \\(x\\) ,那么一定可以将其继续划分为 \\(2(x-2)\\) ,从而获得更大(或相等)的乘积。这与假设矛盾。
    2. 切分方案不包含 \\(1\\) :假设最优切分方案中存在一个因子 \\(1\\) ,那么它一定可以合并入另外一个因子中,以获得更大的乘积。这与假设矛盾。
    3. 切分方案最多包含两个 \\(2\\) :假设最优切分方案中包含三个 \\(2\\) ,那么一定可以替换为两个 \\(3\\) ,乘积更大。这与假设矛盾。
    ","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 章   哈希表"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"本章内容","text":"
    • 6.1   哈希表
    • 6.2   哈希冲突
    • 6.3   哈希算法
    • 6.4   小结
    ","path":["第 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 码累积到哈希值中。
    • 异或哈希:将输入数据的每个元素通过异或操作累积到一个哈希值中。
    • 旋转哈希:将每个字符的 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    \"\"\"异或哈希\"\"\"\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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/* 异或哈希 */\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### 异或哈希 ###\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\\) 整除,那么所有可以被 \\(3\\) 整除的 key 都会被映射到 \\(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\\) ,由于 keymodulus 之间不存在公约数,因此输出的哈希值的均匀性会明显提升。

    \\[ \\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":"

    不难发现,以上介绍的简单哈希算法都比较“脆弱”,远远没有达到哈希算法的设计目标。例如,由于加法和异或满足交换律,因此加法哈希和异或哈希无法区分内容相同但顺序不同的字符串,这可能会加剧哈希冲突,并引起一些安全问题。

    在实际中,我们通常会用一些标准哈希算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。

    近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。表 6-2 展示了在实际应用中常见的哈希算法。

    • MD5 和 SHA-1 已多次被成功攻击,因此它们被各类安全应用弃用。
    • SHA-2 系列中的 SHA-256 是最安全的哈希算法之一,仍未出现成功的攻击案例,因此常用在各类安全应用与协议中。
    • 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 算法\"\nhash_str = hash(str)\n# 字符串“Hello 算法”的哈希值为 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 算法\";\nsize_t hashStr = hash<string>()(str);\n// 字符串“Hello 算法”的哈希值为 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 算法\";\nint hashStr = str.hashCode();\n// 字符串“Hello 算法”的哈希值为 -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 算法\";\nint hashStr = str.GetHashCode();\n// 字符串“Hello 算法”的哈希值为 -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 算法\"\nlet hashStr = str.hashValue\n// 字符串“Hello 算法”的哈希值为 -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 算法\";\nint hashStr = str.hashCode;\n// 字符串“Hello 算法”的哈希值为 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 算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 字符串“Hello 算法”的哈希值为 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 算法\"\nval hashStr = str.hashCode()\n// 字符串“Hello 算法”的哈希值为 -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 算法\"\nhash_str = str.hash\n# 字符串“Hello 算法”的哈希值为 -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
    可视化运行

    全屏观看 >

    在许多编程语言中,只有不可变对象才可作为哈希表的 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 来标记这个桶。在该机制下,NoneTOMBSTONE 都代表空桶,都可以放置键值对。但不同的是,线性探测到 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 个键值对,超出容量则连接一个溢出桶;当溢出桶过多时,会执行一次特殊的等量扩容操作,以确保性能。
    ","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
    可视化运行

    全屏观看 >

    哈希表有三种常用的遍历方式:遍历键值对、遍历键和遍历值。示例代码如下:

    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
    可视化运行

    全屏观看 >

    ","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 对应的键值对在数组中的存储位置。

    输入一个 key ,哈希函数的计算过程分为以下两步。

    1. 通过某种哈希算法 hash() 计算得到哈希值。
    2. 将哈希值对桶数量(数组长度)capacity 取模,从而获取该 key 对应的桶(数组索引)index
    index = hash(key) % capacity\n

    随后,我们就可以利用 index 在哈希表中访问对应的桶,从而获取 value

    设数组长度 capacity = 100、哈希算法 hash(key) = key ,易得哈希函数为 key % 100 。图 6-2 以 key 学号和 value 姓名为例,展示了哈希函数的工作原理。

    图 6-2   哈希函数工作原理

    以下代码实现了一个简单哈希表。其中,我们将 keyvalue 封装成一个类 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 ,哈希表能够在 \\(O(1)\\) 时间内查询到 value ,效率非常高。
    • 常见的哈希表操作包括查询、添加键值对、删除键值对和遍历哈希表等。
    • 哈希函数将 key 映射为数组索引,从而访问对应桶并获取 value
    • 两个不同的 key 可能在经过哈希函数后得到相同的数组索引,导致查询结果出错,这种现象被称为哈希冲突。
    • 哈希表容量越大,哈希冲突的概率就越低。因此可以通过扩容哈希表来缓解哈希冲突。与数组扩容类似,哈希表扩容操作的开销很大。
    • 负载因子定义为哈希表中元素数量除以桶数量,反映了哈希冲突的严重程度,常用作触发哈希表扩容的条件。
    • 链式地址通过将单个元素转化为链表,将所有冲突元素存储在同一个链表中。然而,链表过长会降低查询效率,可以通过进一步将链表转换为红黑树来提高效率。
    • 开放寻址通过多次探测来处理哈希冲突。线性探测使用固定步长,缺点是不能删除元素,且容易产生聚集。多次哈希使用多个哈希函数进行探测,相较线性探测更不易产生聚集,但多个哈希函数增加了计算量。
    • 不同编程语言采取了不同的哈希表实现。例如,Java 的 HashMap 使用链式地址,而 Python 的 Dict 采用开放寻址。
    • 在哈希表中,我们希望哈希算法具有确定性、高效率和均匀分布的特点。在密码学中,哈希算法还应该具备抗碰撞性和雪崩效应。
    • 哈希算法通常采用大质数作为模数,以最大化地保证哈希值均匀分布,减少哈希冲突。
    • 常见的哈希算法包括 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 章   堆"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"本章内容","text":"
    • 8.1   堆
    • 8.2   建堆操作
    • 8.3   Top-k 问题
    • 8.4   小结
    ","path":["第 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    # 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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        // 堆化除叶节点以外的其他所有节点\n        h.siftDown(i)\n    }\n    return h\n}\n
    my_heap.swift
    /* 构造方法,根据输入列表建堆 */\ninit(nums: [Int]) {\n    // 将列表元素原封不动添加进堆\n    maxHeap = nums\n    // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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  // 堆化除叶节点以外的其他所有节点\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    // 堆化除叶节点以外的其他所有节点\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        // 堆化除叶节点以外的其他所有节点\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        // 堆化除叶节点以外的其他所有节点\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            // 当“越过根节点”或“节点无须修复”时,结束堆化\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  # 堆化除叶节点以外的其他所有节点\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)\\) 。但这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的性质。

    接下来我们来进行更为准确的计算。为了降低计算难度,假设给定一个节点数量为 \\(n\\) 、高度为 \\(h\\) 的“完美二叉树”,该假设不会影响计算结果的正确性。

    图 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() 访问堆顶元素(对于大 / 小顶堆分别为最大 / 小值) \\(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# Python 的 heapq 模块默认实现小顶堆\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// 初始化大顶堆(使用 lambda 表达式修改 Comparator 即可)\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// 初始化大顶堆(使用 lambda 表达式修改 Comparer 即可)\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// Swift 的 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// 初始化大顶堆(使用 lambda 表达式修改 Comparator 即可)\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
    可视化运行

    全屏观看 >

    ","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 可能大于堆中其他元素,堆的成立条件可能已被破坏,因此需要修复从插入节点到根节点的路径上的各个节点,这个操作被称为堆化(heapify)。

    考虑从入堆节点开始,从底至顶执行堆化。如图 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        # 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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    // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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        // 当“越过根节点”或“节点无须修复”时,结束堆化\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    # 当“越过根节点”或“节点无须修复”时,结束堆化\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

    给定一个长度为 \\(n\\) 的无序数组 nums ,请返回数组中最大的 \\(k\\) 个元素。

    对于该问题,我们先介绍两种思路比较直接的解法,再介绍效率更高的堆解法。

    ","path":["第 8 章   堆","8.3   Top-k 问题"],"tags":[]},{"location":"chapter_heap/top_k/#831","level":2,"title":"8.3.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","level":2,"title":"8.3.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","level":2,"title":"8.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":"

    几年前,我在力扣上分享了“剑指 Offer”系列题解,受到了许多读者的鼓励和支持。在与读者交流期间,我最常被问的一个问题是“如何入门算法”。逐渐地,我对这个问题产生了浓厚的兴趣。

    两眼一抹黑地刷题似乎是最受欢迎的方法,简单、直接且有效。然而刷题就如同玩“扫雷”游戏,自学能力强的人能够顺利将地雷逐个排掉,而基础不足的人很可能被炸得满头是包,并在挫折中步步退缩。通读教材也是一种常见做法,但对于面向求职的人来说,毕业论文、投递简历、准备笔试和面试已经消耗了大部分精力,啃厚重的书往往变成了一项艰巨的挑战。

    如果你也面临类似的困扰,那么很幸运这本书“找”到了你。本书是我对这个问题给出的答案,即使不是最优解,也至少是一次积极的尝试。本书虽然不足以让你直接拿到 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 章   初识算法"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"本章内容","text":"
    • 1.1   算法无处不在
    • 1.2   算法是什么
    • 1.3   小结
    ","path":["第 1 章   初识算法"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   算法无处不在","text":"

    当我们听到“算法”这个词时,很自然地会想到数学。然而实际上,许多算法并不涉及复杂数学,而是更多地依赖基本逻辑,这些逻辑在我们的日常生活中处处可见。

    在正式探讨算法之前,有一个有趣的事实值得分享:你已经在不知不觉中学会了许多算法,并习惯将它们应用到日常生活中了。下面我将举几个具体的例子来证实这一点。

    例一:查字典。在字典里,每个汉字都对应一个拼音,而字典是按照拼音字母顺序排列的。假设我们需要查找一个拼音首字母为 \\(r\\) 的字,通常会按照图 1-1 所示的方式实现。

    1. 翻开字典约一半的页数,查看该页的首字母是什么,假设首字母为 \\(m\\) 。
    2. 由于在拼音字母表中 \\(r\\) 位于 \\(m\\) 之后,所以排除字典前半部分,查找范围缩小到后半部分。
    3. 不断重复步骤 1. 和步骤 2. ,直至找到拼音首字母为 \\(r\\) 的页码为止。
    <1><2><3><4><5>

    图 1-1   查字典步骤

    查字典这个小学生必备技能,实际上就是著名的“二分查找”算法。从数据结构的角度,我们可以把字典视为一个已排序的“数组”;从算法的角度,我们可以将上述查字典的一系列操作看作“二分查找”。

    例二:整理扑克。我们在打牌时,每局都需要整理手中的扑克牌,使其从小到大排列,实现流程如图 1-2 所示。

    1. 将扑克牌划分为“有序”和“无序”两部分,并假设初始状态下最左 1 张扑克牌已经有序。
    2. 在无序部分抽出一张扑克牌,插入至有序部分的正确位置;完成后最左 2 张扑克已经有序。
    3. 不断循环步骤 2. ,每一轮将一张扑克牌从无序部分插入至有序部分,直至所有扑克牌都有序。

    图 1-2   扑克排序步骤

    上述整理扑克牌的方法本质上是“插入排序”算法,它在处理小型数据集时非常高效。许多编程语言的排序库函数中都有插入排序的身影。

    例三:货币找零。假设我们在超市购买了 \\(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_paperbook/","level":1,"title":"纸质书","text":"

    经过长时间的打磨,《Hello 算法》纸质书终于发布了!此时的心情可以用一句诗来形容:

    追风赶月莫停留,平芜尽处是春山。

    以下视频展示了纸质书,并且包含我的一些思考:

    • 学习数据结构与算法的重要性。
    • 为什么在纸质书中选择 Python。
    • 对知识分享的理解。

    新人 UP 主,请多多关照、一键三连~谢谢!

    附纸质书快照:

    ","path":["纸质书"],"tags":[]},{"location":"chapter_paperbook/#_2","level":2,"title":"优势与不足","text":"

    总结一下纸质书可能会给大家带来惊喜的地方:

    • 采用全彩印刷,能够原汁原味地发挥出本书“动画图解”的优势。
    • 考究纸张材质,既保证色彩高度还原,也保留纸质书特有的质感。
    • 纸质版比网页版的格式更加规范,例如图中的公式使用斜体。
    • 在不提升定价的前提下,附赠思维导图折页、书签。
    • 纸质书、网页版、PDF 版内容同步,随意切换阅读。

    Tip

    由于纸质书和网页版的同步难度较大,因此可能会有一些细节上的不同,请您见谅!

    当然,纸质书也有一些值得大家入手前考虑的地方:

    • 使用 Python 语言,可能不匹配你的主语言(可以把 Python 看作伪代码,重在理解思路)。
    • 全彩印刷虽然大幅提升了图解和代码的阅读体验,但价格会比黑白印刷高一些。

    Tip

    “印刷质量”和“价格”就像算法中的“时间效率”和“空间效率”,难以两全。而我认为,“印刷质量”对应的是“时间效率”,更应该被注重。

    ","path":["纸质书"],"tags":[]},{"location":"chapter_paperbook/#_3","level":2,"title":"购买链接","text":"

    如果你对纸质书感兴趣,可以考虑入手一本。我们为大家争取到了新书 5 折优惠,请见此链接或扫描以下二维码:

    ","path":["纸质书"],"tags":[]},{"location":"chapter_paperbook/#_4","level":2,"title":"尾记","text":"

    起初,我低估了纸质书出版的工作量,以为只要维护好了开源项目,纸质版就可以通过某些自动化手段生成出来。实践证明,纸质书的生产流程与开源项目的更新机制存在很大的不同,两者之间的转化需要做许多额外工作。

    一本书的初稿与达到出版标准的定稿之间仍有较长距离,需要出版社(策划、编辑、设计、市场等)与作者的通力合作、长期雕琢。在此感谢图灵策划编辑王军花、以及人民邮电出版社和图灵社区每位参与本书出版流程的工作人员!

    希望这本书能够帮助到你!

    ","path":["纸质书"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"第 0 章   前言","text":"

    Abstract

    算法犹如美妙的交响乐,每一行代码都像韵律般流淌。

    愿这本书在你的脑海中轻轻响起,留下独特而深刻的旋律。

    ","path":["第 0 章   前言"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"本章内容","text":"
    • 0.1   关于本书
    • 0.2   如何使用本书
    • 0.3   小结
    ","path":["第 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 开发。感谢他的贡献,为读者提供了更灵活的阅读方式。

    在本书的创作过程中,我得到了许多人的帮助。

    • 感谢我在公司的导师李汐博士,在一次畅谈中你鼓励我“快行动起来”,坚定了我写这本书的决心;
    • 感谢我的女朋友泡泡作为本书的首位读者,从算法小白的角度提出许多宝贵建议,使得本书更适合新手阅读;
    • 感谢腾宝、琦宝、飞宝为本书起了一个富有创意的名字,唤起大家写下第一行代码“Hello World!”的美好回忆;
    • 感谢校铨在知识产权方面提供的专业帮助,这对本开源书的完善起到了重要作用;
    • 感谢苏潼为本书设计了精美的封面和 logo ,并在我的强迫症的驱使下多次耐心修改;
    • 感谢 @squidfunk 提供的排版建议,以及他开发的开源文档主题 Material-for-MkDocs 。

    在写作过程中,我阅读了许多关于数据结构与算法的教材和文章。这些作品为本书提供了优秀的范本,确保了本书内容的准确性与品质。在此感谢所有老师和前辈的杰出贡献!

    本书倡导手脑并用的学习方式,在这一点上我深受《动手学深度学习》的启发。在此向各位读者强烈推荐这本优秀的著作。

    衷心感谢我的父母,正是你们一直以来的支持与鼓励,让我有机会做这件富有趣味的事。

    ","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   运行代码示例

    运行代码的前置工作主要分为三步。

    第一步:安装本地编程环境。请参照附录所示的教程进行安装,如果已安装,则可跳过此步骤。

    第二步:克隆或下载代码仓库。前往 GitHub 仓库。如果已经安装 Git ,可以通过以下命令克隆本仓库:

    git clone https://github.com/krahets/hello-algo.git\n

    当然,你也可以在图 0-4 所示的位置,点击“Download ZIP”按钮直接下载代码压缩包,然后在本地解压即可。

    图 0-4   克隆仓库与下载代码

    第三步:运行源代码。如图 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. 阶段一:算法入门。我们需要熟悉各种数据结构的特点和用法,学习不同算法的原理、流程、用途和效率等方面的内容。
    2. 阶段二:刷算法题。建议从热门题目开刷,先积累至少 100 道题目,熟悉主流的算法问题。初次刷题时,“知识遗忘”可能是一个挑战,但请放心,这是很正常的。我们可以按照“艾宾浩斯遗忘曲线”来复习题目,通常在进行 3~5 轮的重复后,就能将其牢记在心。推荐的题单和刷题计划请见此 GitHub 仓库。
    3. 阶段三:搭建知识体系。在学习方面,我们可以阅读算法专栏文章、解题框架和算法教材,以不断丰富知识体系。在刷题方面,可以尝试采用进阶刷题策略,如按专题分类、一题多解、一解多题等,相关的刷题心得可以在各个社区找到。

    如图 0-8 所示,本书内容主要涵盖“阶段一”,旨在帮助你更高效地展开阶段二和阶段三的学习。

    图 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, et al. Introduction to Algorithms (3rd Edition).

    [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

    [3] Robert Sedgewick, et al. Algorithms (4th Edition).

    [4] 严蔚敏. 数据结构(C 语言版).

    [5] 邓俊辉. 数据结构(C++ 语言版,第三版).

    [6] 马克 艾伦 维斯著,陈越译. 数据结构与算法分析:Java语言描述(第三版).

    [7] 程杰. 大话数据结构.

    [8] 王争. 数据结构与算法之美.

    [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

    [10] Aston Zhang, et al. Dive into Deep Learning.

    ","path":["参考文献"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章   搜索","text":"

    Abstract

    搜索是一场未知的冒险,我们或许需要走遍神秘空间的每个角落,又或许可以快速锁定目标。

    在这场寻觅之旅中,每一次探索都可能得到一个未曾料想的答案。

    ","path":["第 10 章   搜索"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"本章内容","text":"
    • 10.1   二分查找
    • 10.2   二分查找插入点
    • 10.3   二分查找边界
    • 10.4   哈希优化策略
    • 10.5   重识搜索算法
    • 10.6   小结
    ","path":["第 10 章   搜索"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   二分查找","text":"

    二分查找(binary search)是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。

    Question

    给定一个长度为 \\(n\\) 的数组 nums ,元素按从小到大的顺序排列且不重复。请查找并返回元素 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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 分别指向数组首元素、尾元素+1\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 次除法、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

    给定一个长度为 \\(n\\) 的有序数组 nums ,其中可能包含重复元素。请返回数组中最左一个元素 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 的元素。

    因此,如图 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

    给定一个长度为 \\(n\\) 的有序数组 nums 和一个元素 target ,数组不存在重复元素。现将 target 插入数组 nums 中,并保持其有序性。若数组中已存在元素 target ,则插入到其左方。请返回插入后 target 在数组中的索引。示例如图 10-4 所示。

    图 10-4   二分查找插入点示例数据

    如果想复用上一节的二分查找代码,则需要回答以下两个问题。

    问题一:当数组中包含 target 时,插入点的索引是否是该元素的索引?

    题目要求将 target 插入到相等元素的左边,这意味着新插入的 target 替换了原来 target 的位置。也就是说,当数组包含 target 时,插入点的索引就是该 target 的索引。

    问题二:当数组中不存在 target 时,插入点是哪个元素的索引?

    进一步思考二分查找过程:当 nums[m] < target 时 \\(i\\) 移动,这意味着指针 \\(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

    题目要求将目标元素插入到最左边,所以我们需要查找数组中最左一个 target 的索引。初步考虑通过图 10-5 所示的步骤实现。

    1. 执行二分查找,得到任意一个 target 的索引,记为 \\(k\\) 。
    2. 从索引 \\(k\\) 开始,向左进行线性遍历,当找到最左边的 target 时返回。

    图 10-5   线性查找重复元素的插入点

    此方法虽然可用,但其包含线性查找,因此时间复杂度为 \\(O(n)\\) 。当数组中存在很多重复的 target 时,该方法效率很低。

    现考虑拓展二分查找代码。如图 10-6 所示,整体流程保持不变,每轮先计算中点索引 \\(m\\) ,再判断 targetnums[m] 的大小关系,分为以下几种情况。

    • nums[m] < targetnums[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] > targetnums[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    \"\"\"方法一:暴力枚举\"\"\"\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一: 暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    /* 方法一:暴力枚举 */\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
    ### 方法一:暴力枚举 ###\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    \"\"\"方法二:辅助哈希表\"\"\"\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    /* 方法二: 辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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/* 方法二:辅助哈希表 */\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
    /* 方法二:辅助哈希表 */\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
    ### 方法二:辅助哈希表 ###\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 章   排序"],"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 章   排序"],"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_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":"

    桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 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 + 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":"

    先来看一个简单的例子。给定一个长度为 \\(n\\) 的数组 nums ,其中的元素都是“非负整数”,计数排序的整体流程如图 11-16 所示。

    1. 遍历数组,找出其中的最大数字,记为 \\(m\\) ,然后创建一个长度为 \\(m + 1\\) 的辅助数组 counter
    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    # 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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  // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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    // 使用结果数组 res 覆盖原数组 nums\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  # 使用结果数组 res 覆盖原数组 nums\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)\\)、非原地排序:借助了长度分别为 \\(n\\) 和 \\(m\\) 的数组 rescounter
    • 稳定排序:由于向 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. 从堆顶元素开始,从顶到底执行堆化操作(sift down)。完成堆化后,堆的性质得到修复。
    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    # 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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  // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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    // 建堆操作:堆化除叶节点以外的其他所有节点\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  # 建堆操作:堆化除叶节点以外的其他所有节点\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. 初始状态下,数组的第 1 个元素已完成排序。
    2. 选取数组的第 2 个元素作为 base ,将其插入到正确位置后,数组的前 2 个元素已排序。
    3. 选取第 3 个元素作为 base ,将其插入到正确位置后,数组的前 3 个元素已排序。
    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. 选取数组最左端元素作为基准数,初始化两个指针 ij 分别指向数组的两端。
    2. 设置一个循环,在每轮中使用 ij)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。
    3. 循环执行步骤 2. ,直到 ij 相遇时停止,最后将基准数交换至两个子数组的分界线。
    <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 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 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   基数排序算法流程

    下面剖析代码实现。对于一个 \\(d\\) 进制的数字 \\(x\\) ,要获取其第 \\(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    \"\"\"获取元素 num 的第 k 位,其中 exp = 10^(k-1)\"\"\"\n    # 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"计数排序(根据 nums 第 k 位排序)\"\"\"\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)  # 获取 nums[i] 第 k 位,记为 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  # 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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) // 获取 nums[i] 第 k 位,记为 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 // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    (num / exp) % 10\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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) // 获取 nums[i] 第 k 位,记为 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 // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 _num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n  return (_num ~/ exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return ((num / exp) % 10) as usize;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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); // 初始化为 0 以支持后续内存释放\n    // 统计 0~9 各数字的出现次数\n    for (int i = 0; i < size; i++) {\n        // 获取 nums[i] 第 k 位,记为 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; // 获取 d 在数组中的索引 j\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
    /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\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) // 获取 nums[i] 第 k 位,记为 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 // 获取 d 在数组中的索引 j\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
    ### 获取元素 num 的第 k 位,其中 exp = 10^(k-1) ###\ndef digit(num, exp)\n  # 转入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n  (num / exp) % 10\nend\n\n### 计数排序(根据 nums 第 k 位排序)###\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) # 获取 nums[i] 第 k 位,记为 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 # 获取 d 在数组中的索引 j\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)\\)、非原地排序:与计数排序相同,基数排序需要借助长度为 \\(n\\) 和 \\(d\\) 的数组 rescounter
    • 稳定排序:当计数排序稳定时,基数排序也稳定;当计数排序不稳定时,基数排序无法保证得到正确的排序结果。
    ","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\\) 处的元素交换。完成后,数组前 1 个元素已排序。
    3. 选取区间 \\([1, n-1]\\) 中的最小元素,将其与索引 \\(1\\) 处的元素交换。完成后,数组前 2 个元素已排序。
    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":"

    运行效率:我们期望排序算法的时间复杂度尽量低,且总体操作数量较少(时间复杂度中的常数项变小)。对于大数据量的情况,运行效率显得尤为重要。

    就地性:顾名思义,原地排序通过在原数组上直接操作实现排序,无须借助额外的辅助数组,从而节省内存。通常情况下,原地排序的数据搬运操作较少,运行速度也更快。

    稳定性:稳定排序在完成排序后,相等元素在数组中的相对顺序不发生改变。

    稳定排序是多级排序场景的必要条件。假设我们有一个存储学生信息的表格,第 1 列和第 2 列分别是姓名和年龄。在这种情况下,非稳定排序可能导致输入数据的有序性丧失:

    # 输入数据是按照姓名排序好的\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-q-a","level":3,"title":"2.   Q & A","text":"

    Q:排序算法稳定性在什么情况下是必需的?

    在现实中,我们有可能基于对象的某个属性进行排序。例如,学生有姓名和身高两个属性,我们希望实现一个多级排序:先按照姓名进行排序,得到 (A, 180) (B, 185) (C, 170) (D, 170) ;再对身高进行排序。由于排序算法不稳定,因此可能得到 (D, 170) (C, 170) (A, 180) (B, 185)

    可以发现,学生 D 和 C 的位置发生了交换,姓名的有序性被破坏了,而这是我们不希望看到的。

    Q:哨兵划分中“从右往左查找”与“从左往右查找”的顺序可以交换吗?

    不行,当我们以最左端元素为基准数时,必须先“从右往左查找”再“从左往右查找”。这个结论有些反直觉,我们来剖析一下原因。

    哨兵划分 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] 为基准数,那么正好反过来,必须先“从左往右查找”。

    Q:关于快速排序的递归深度优化,为什么选短的数组能保证递归深度不超过 \\(\\log n\\) ?

    递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在递归深度优化后,向下递归的子数组长度最大为原数组长度的一半。假设最差情况,一直为一半长度,那么最终的递归深度就是 \\(\\log n\\) 。

    回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) ,递归深度为 \\(n\\) 。递归深度优化可以避免这种情况出现。

    Q:当数组中所有元素都相等时,快速排序的时间复杂度是 \\(O(n^2)\\) 吗?该如何处理这种退化情况?

    是的。对于这种情况,可以考虑通过哨兵划分将数组划分为三个部分:小于、等于、大于基准数。仅向下递归小于和大于的两部分。在该方法下,输入元素全部相等的数组,仅一轮哨兵划分即可完成排序。

    Q:桶排序的最差时间复杂度为什么是 \\(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 章   栈与队列"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"本章内容","text":"
    • 5.1   栈
    • 5.2   队列
    • 5.3   双向队列
    • 5.4   小结
    ","path":["第 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
    可视化运行

    全屏观看 >

    ","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":"

    回顾上一节内容,我们使用普通单向链表来实现队列,因为它可以方便地删除头节点(对应出队操作)和在尾节点后添加新节点(对应入队操作)。

    对于双向队列而言,头部和尾部都可以执行入队和出队操作。换句话说,双向队列需要实现另一个对称方向的操作。为此,我们采用“双向链表”作为双向队列的底层数据结构。

    如图 5-8 所示,我们将双向链表的头节点和尾节点视为双向队列的队首和队尾,同时实现在两端添加和删除节点的功能。

    <1><2><3><4><5>

    图 5-8   基于链表实现双向队列的入队出队操作

    实现代码如下所示:

    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   基于数组实现双向队列的入队出队操作

    在队列的实现基础上,仅需增加“队首入队”和“队尾出队”的方法:

    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;        // 队首指针,指向队首元素\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; // 队首指针,指向队首元素\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;   // 队首指针,指向队首元素\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   // 队首指针,指向队首元素\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 // 队首指针,指向队首元素\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; // 队首指针,指向队首元素\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; // 队首指针,指向队首元素\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; // 队首指针,指向队首元素\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,    // 队首指针,指向队首元素\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;       // 队首指针,指向队首元素\n    int queSize;     // 尾指针,指向队尾 + 1\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 // 队首指针,指向队首元素\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":"

    双向队列兼具栈与队列的逻辑,因此它可以实现这两者的所有应用场景,同时提供更高的自由度。

    我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 push 到栈中,然后通过 pop 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 \\(50\\) 步)。当栈的长度超过 \\(50\\) 时,软件需要在栈底(队首)执行删除操作。但栈无法实现该功能,此时就需要使用双向队列来替代栈。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。

    ","path":["第 5 章   栈与队列","5.3   双向队列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   队列","text":"

    队列(queue)是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。

    如图 5-4 所示,我们将队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。

    图 5-4   队列的先入先出规则

    ","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 中,队列类 Qeque 是双向队列,也可作为队列使用\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 和遍历方法,可以把 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
    可视化运行

    全屏观看 >

    ","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   基于链表实现队列的入队出队操作

    以下是用链表实现队列的代码:

    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        # 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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    // 如果队列为空,则令头、尾节点都指向该节点\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            // 如果队列为空,则令头、尾节点都指向该节点\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    // 如果队列为空,则令头、尾节点都指向该节点\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        // 如果队列为空,则令头、尾节点都指向该节点\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    # 如果队列为空,则令头,尾节点都指向该节点\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)\\) ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。

    我们可以使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。

    基于此设计,数组中包含元素的有效区间为 [front, rear - 1],各种操作的实现方法如图 5-6 所示。

    • 入队操作:将输入元素赋值给 rear 索引处,并将 size 增加 1 。
    • 出队操作:只需将 front 增加 1 ,并将 size 减少 1 。

    可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 \\(O(1)\\) 。

    <1><2><3>

    图 5-6   基于数组实现队列的入队出队操作

    你可能会发现一个问题:在不断进行入队和出队的过程中,frontrear 都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。

    对于环形数组,我们需要让 frontrear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示:

    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  # 队首指针,指向队首元素\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        # 队首指针向后移动一位,若越过尾部,则返回到数组头部\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;       // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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; // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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;   // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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   // 队首指针,指向队首元素\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    // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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 // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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; // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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; // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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; // 队首指针,指向队首元素\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    // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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,        // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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;       // 队首指针,指向队首元素\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    // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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 // 队首指针,指向队首元素\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        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\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 # 队首指针,指向队首元素\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    # 队首指针向后移动一位,若越过尾部,则返回到数组头部\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 所示,我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫作“入栈”,删除栈顶元素的操作叫作“出栈”。

    图 5-1   栈的先入后出规则

    ","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
    可视化运行

    全屏观看 >

    ","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   栈的实现","text":"

    为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。

    栈遵循先入后出的原则,因此我们只能在栈顶添加或删除元素。然而,数组和链表都可以在任意位置添加和删除元素,因此栈可以视为一种受限制的数组或链表。换句话说,我们可以“屏蔽”数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性。

    ","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   基于链表的实现","text":"

    使用链表实现栈时,我们可以将链表的头节点视为栈顶,尾节点视为栈底。

    如图 5-2 所示,对于入栈操作,我们只需将元素插入链表头部,这种节点插入方法被称为“头插法”。而对于出栈操作,只需将头节点从链表中删除即可。

    <1><2><3>

    图 5-2   基于链表实现栈的入栈出栈操作

    以下是基于链表实现栈的示例代码:

    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 所示,入栈与出栈操作分别对应在数组尾部添加元素与删除元素,时间复杂度都为 \\(O(1)\\) 。

    <1><2><3>

    图 5-3   基于数组实现栈的入栈出栈操作

    由于入栈的元素可能会源源不断地增加,因此我们可以使用动态数组,这样就无须自行处理数组扩容问题。以下为示例代码:

    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":"

    支持操作

    两种实现都支持栈定义中的各项操作。数组实现额外支持随机访问,但这已超出了栈的定义范畴,因此一般不会用到。

    时间效率

    在基于数组的实现中,入栈和出栈操作都在预先分配好的连续内存中进行,具有很好的缓存本地性,因此效率较高。然而,如果入栈时超出数组容量,会触发扩容机制,导致该次入栈操作的时间复杂度变为 \\(O(n)\\) 。

    在基于链表的实现中,链表的扩容非常灵活,不存在上述数组扩容时效率降低的问题。但是,入栈操作需要初始化节点对象并修改指针,因此效率相对较低。不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率。

    综上所述,当入栈与出栈操作的元素是基本数据类型时,例如 intdouble ,我们可以得出以下结论。

    • 基于数组实现的栈在触发扩容时效率会降低,但由于扩容是低频操作,因此平均效率更高。
    • 基于链表实现的栈可以提供更加稳定的效率表现。

    空间效率

    在初始化列表时,系统会为列表分配“初始容量”,该容量可能超出实际需求;并且,扩容机制通常是按照特定倍率(例如 2 倍)进行扩容的,扩容后的容量也可能超出实际需求。因此,基于数组实现的栈可能造成一定的空间浪费。

    然而,由于链表节点需要额外存储指针,因此链表节点占用的空间相对较大。

    综上,我们不能简单地确定哪种实现更加节省内存,需要针对具体情况进行分析。

    ","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   栈的典型应用","text":"
    • 浏览器中的后退与前进、软件中的撤销与反撤销。每当我们打开新的网页,浏览器就会对上一个网页执行入栈,这样我们就可以通过后退操作回到上一个网页。后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么需要两个栈来配合实现。
    • 程序内存管理。每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会不断执行出栈操作。
    ","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":"
    • 栈是一种遵循先入后出原则的数据结构,可通过数组或链表来实现。
    • 在时间效率方面,栈的数组实现具有较高的平均效率,但在扩容过程中,单次入栈操作的时间复杂度会劣化至 \\(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:在出栈后,是否需要释放出栈节点的内存?

    如果后续仍需要使用弹出节点,则不需要释放内存。若之后不需要用到,JavaPython 等语言拥有自动垃圾回收机制,因此不需要手动释放内存;在 CC++ 中需要手动释放内存。

    Q:双向队列像是两个栈拼接在了一起,它的用途是什么?

    双向队列就像是栈和队列的组合或两个栈拼在了一起。它表现的是栈 + 队列的逻辑,因此可以实现栈与队列的所有应用,并且更加灵活。

    Q:撤销(undo)和反撤销(redo)具体是如何实现的?

    使用两个栈,栈 A 用于撤销,栈 B 用于反撤销。

    1. 每当用户执行一个操作,将这个操作压入栈 A ,并清空栈 B
    2. 当用户执行“撤销”时,从栈 A 中弹出最近的操作,并将其压入栈 B
    3. 当用户执行“反撤销”时,从栈 B 中弹出最近的操作,并将其压入栈 A
    ","path":["第 5 章   栈与队列","5.4   小结"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章   树","text":"

    Abstract

    参天大树充满生命力,根深叶茂,分枝扶疏。

    它为我们展现了数据分治的生动形态。

    ","path":["第 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 章   树"],"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 ,因此我们无法仅凭该序列来推测 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// 使用 int 的包装类 Integer ,就可以使用 null 来标记空位\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
    /* 二叉树的数组表示 */\n// 使用 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// 使用 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// 使用 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 年 G. M. Adelson-Velsky 和 E. M. 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 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种平衡二叉搜索树(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    # 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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  // 以 child 为原点,将 node 向右旋转\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            // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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    // 以 child 为原点,将 node 向右旋转\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  # 以 child 为原点,将 node 向右旋转\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_1","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    # 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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  // 以 child 为原点,将 node 向左旋转\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            // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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    // 以 child 为原点,将 node 向左旋转\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  # 以 child 为原点,将 node 向左旋转\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":"

    对于图 7-30 中的失衡节点 3 ,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先对 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-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_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_2","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_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.valnum 之间的大小关系。

    • 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 or 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        # 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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    // 用 tmp 覆盖 cur\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 or 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            // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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        // 用 tmp 覆盖 cur\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 or 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    # 用 tmp 覆盖 cur\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
    可视化运行

    全屏观看 >

    ","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# 在 n1 -> n2 中间插入节点 P\nn1.left = p\np.left = n2\n# 删除节点 P\nn1.left = n2\n
    binary_tree.cpp
    /* 插入与删除节点 */\nTreeNode* P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1->left = P;\nP->left = n2;\n// 删除节点 P\nn1->left = n2;\n// 释放内存\ndelete P;\n
    binary_tree.java
    TreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.cs
    /* 插入与删除节点 */\nTreeNode P = new(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.go
    /* 插入与删除节点 */\n// 在 n1 -> n2 中间插入节点 P\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// 删除节点 P\nn1.Left = n2\n
    binary_tree.swift
    let P = TreeNode(x: 0)\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P\nP.left = n2\n// 删除节点 P\nn1.left = n2\n
    binary_tree.js
    /* 插入与删除节点 */\nlet P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.ts
    /* 插入与删除节点 */\nconst P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.dart
    /* 插入与删除节点 */\nTreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
    binary_tree.rs
    let p = TreeNode::new(0);\n// 在 n1 -> n2 中间插入节点 P\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// 在 n1 -> n2 中间插入节点 P\nn1->left = P;\nP->left = n2;\n// 删除节点 P\nn1->left = n2;\n// 释放内存\nfree(P);\n
    binary_tree.kt
    val P = TreeNode(0)\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P\nP.left = n2\n// 删除节点 P\nn1.left = n2\n
    binary_tree.rb
    # 插入与删除节点\n_p = TreeNode.new(0)\n# 在 n1 -> n2 中间插入节点 _p\nn1.left = _p\n_p.left = n2\n# 删除节点\nn1.left = n2\n
    可视化运行

    全屏观看 >

    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:右旋操作是处理失衡节点 nodechildgrand_child 之间的关系,那 node 的父节点和 node 原来的连接不需要维护吗?右旋操作后岂不是断掉了?

    我们需要从递归的视角来看这个问题。右旋操作 right_rotate(root) 传入的是子树的根节点,最终 return child 返回旋转之后的子树的根节点。子树的根节点和其父节点的连接是在该函数返回后完成的,不属于右旋操作的维护范围。

    Q:在 C++ 中,函数被划分到 privatepublic 中,这方面有什么考量吗?为什么要将 height() 函数和 updateHeight() 函数分别放在 publicprivate 中呢?

    主要看方法的使用范围,如果方法只在类内部使用,那么就设计为 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/stylesheets/animation_player.css b/stylesheets/animation_player.css index aee5c7ab4..624e01ac3 100644 --- a/stylesheets/animation_player.css +++ b/stylesheets/animation_player.css @@ -176,4 +176,4 @@ font-size: 0.7rem; } } -/*! update cache: 20260410024918 */ +/*! update cache: 20260410223920 */ diff --git a/stylesheets/extra.css b/stylesheets/extra.css index 507f2a911..d7c488fb1 100644 --- a/stylesheets/extra.css +++ b/stylesheets/extra.css @@ -790,4 +790,4 @@ a:hover .device-on-hover { flex: 1 1 30%; } } -/*! update cache: 20260410024918 */ +/*! update cache: 20260410223920 */ diff --git a/stylesheets/giscus-dark.css b/stylesheets/giscus-dark.css index 17d7c6ed3..f13096a0a 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: 20260410024918 */ +/*! update cache: 20260410223920 */ diff --git a/stylesheets/giscus-light.css b/stylesheets/giscus-light.css index 8aa1eab41..d55e7cbd7 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: 20260410024918 */ +/*! update cache: 20260410223920 */ diff --git a/zh-hant/assets/javascripts/bundle.c2b142ea.min.js b/zh-hant/assets/javascripts/bundle.c2b142ea.min.js index f32350283..978524c7b 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: 20260410024928 */ +/*! update cache: 20260410223931 */ diff --git a/zh-hant/chapter_data_structure/character_encoding/index.html b/zh-hant/chapter_data_structure/character_encoding/index.html index 8227dfdf5..5b72c7fa6 100644 --- a/zh-hant/chapter_data_structure/character_encoding/index.html +++ b/zh-hant/chapter_data_structure/character_encoding/index.html @@ -4445,9 +4445,8 @@

    3.4.3   Unicode 字元集

    隨著計算機技術的蓬勃發展,字元集與編碼標準百花齊放,而這帶來了許多問題。一方面,這些字元集一般只定義了特定語言的字元,無法在多語言環境下正常工作。另一方面,同一種語言存在多種字元集標準,如果兩臺計算機使用的是不同的編碼標準,則在資訊傳遞時就會出現亂碼。

    那個時代的研究人員就在想:如果推出一個足夠完整的字元集,將世界範圍內的所有語言和符號都收錄其中,不就可以解決跨語言環境和亂碼問題了嗎?在這種想法的驅動下,一個大而全的字元集 Unicode 應運而生。

    -

    Unicode 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字元集之中,提供一種通用的字元集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。

    -

    自 1991 年釋出以來,Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月,Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。在龐大的 Unicode 字元集中,常用的字元佔用 2 位元組,有些生僻的字元佔用 3 位元組甚至 4 位元組。

    -

    Unicode 是一種通用字元集,本質上是給每個字元分配一個編號(稱為“碼點”),但它並沒有規定在計算機中如何儲存這些字元碼點。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元?

    +

    Unicode 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字元集之中,提供一種通用的字元集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。自 1991 年釋出以來,Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月,Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。

    +

    Unicode 作為一種通用字元集,本質上是給每個字元分配唯一的“碼點”(字元編號),其取值範圍為 U+0000 至 U+10FFFF,構成了統一的字元編號空間。然而,Unicode 並沒有規定在計算機中如何儲存這些字元碼點。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元?

    對於以上問題,一種直接的解決方案是將所有字元儲存為等長的編碼。如圖 3-7 所示,“Hello”中的每個字元佔用 1 位元組,“演算法”中的每個字元佔用 2 位元組。我們可以透過高位填 0 將“Hello 演算法”中的所有字元都編碼為 2 位元組長度。這樣系統就可以每隔 2 位元組解析一個字元,恢復這個短語的內容了。

    Unicode 編碼示例

    圖 3-7   Unicode 編碼示例

    diff --git a/zh-hant/javascripts/animation_player.js b/zh-hant/javascripts/animation_player.js index 598f59163..ce402db77 100644 --- a/zh-hant/javascripts/animation_player.js +++ b/zh-hant/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410024928 */ +/*! update cache: 20260410223931 */ diff --git a/zh-hant/javascripts/katex.js b/zh-hant/javascripts/katex.js index ccb52bdb1..a3221c4b3 100644 --- a/zh-hant/javascripts/katex.js +++ b/zh-hant/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410024928 */ +/*! update cache: 20260410223931 */ diff --git a/zh-hant/javascripts/mathjax.js b/zh-hant/javascripts/mathjax.js index 67a5bede9..08b2b51a9 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: 20260410024928 */ +/*! update cache: 20260410223931 */ diff --git a/zh-hant/javascripts/starfield.js b/zh-hant/javascripts/starfield.js index 1ec4e1cdc..3c802a2b1 100644 --- a/zh-hant/javascripts/starfield.js +++ b/zh-hant/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410024928 */ +/*! update cache: 20260410223931 */ diff --git a/zh-hant/search.json b/zh-hant/search.json index b6092464e..fd1ff04ee 100644 --- a/zh-hant/search.json +++ b/zh-hant/search.json @@ -1 +1 @@ -{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"第 16 章   附錄","text":"","path":["第 16 章   附錄"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"本章內容","text":"
    • 16.1   程式設計環境安裝
    • 16.2   一起參與創作
    • 16.3   術語表
    ","path":["第 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. 點選“編輯圖示”,如果遇到“需要 Fork 此倉庫”的提示,請同意該操作。
    2. 修改 Markdown 源檔案內容,檢查內容的正確性,並儘量保持排版格式的統一。
    3. 在頁面底部填寫修改說明,然後點選“Propose file change”按鈕。頁面跳轉後,點選“Create 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. 將本地所做更改 Commit ,然後 Push 至遠端倉庫。
    5. 重新整理倉庫網頁,點選“Create 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":"

    推薦使用開源、輕量的 VS Code 作為本地整合開發環境(IDE)。訪問 VS Code 官網,根據作業系統選擇相應版本的 VS Code 進行下載和安裝。

    圖 16-1   從官網下載 VS Code

    VS Code 擁有強大的擴展包生態系統,支持大多數程式語言的執行和除錯。以 Python 為例,安裝“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 大 \\(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 AVL 樹 red-black tree 紅黑樹 level-order traversal 層序走訪 breadth-first traversal 廣度優先走訪 depth-first traversal 深度優先走訪 binary search tree 二元搜尋樹 balanced binary search tree 平衡二元搜尋樹 balance factor 平衡因子 heap 堆積 max heap 大頂堆積 min heap 小頂堆積 priority queue 優先佇列 heapify 堆積化 top-\\(k\\) problem Top-\\(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 章   陣列與鏈結串列"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"本章內容","text":"
    • 4.1   陣列
    • 4.2   鏈結串列
    • 4.3   串列
    • 4.4   記憶體與快取 *
    • 4.5   小結
    ","path":["第 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":"

    我們可以根據需求選用陣列的兩種初始化方式:無初始值、給定初始值。在未指定初始值的情況下,大多數程式語言會將陣列元素初始化為 \\(0\\) :

    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
    視覺化執行

    全螢幕觀看 >

    ","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    \"\"\"在陣列的索引 index 處插入元素 num\"\"\"\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 _num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    ### 在陣列的索引 index 處插入元素 num ###\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// 請注意,JavaScript 的 Array 是動態陣列,可以直接擴展\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// 請注意,TypeScript 的 Array 是動態陣列,可以直接擴展\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# 請注意,Ruby 的 Array 是動態陣列,可以直接擴展\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 中分別被記為 nullnullptrNone
    • 在 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
    視覺化執行

    全螢幕觀看 >

    陣列整體是一個變數,比如陣列 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 所示,假設我們想在相鄰的兩個節點 n0n1 之間插入一個新節點 P ,則只需改變兩個節點引用(指標)即可,時間複雜度為 \\(O(1)\\) 。

    相比之下,在陣列中插入元素的時間複雜度為 \\(O(n)\\) ,在大資料量下的效率較低。

    圖 4-6   鏈結串列插入節點示例

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def insert(n0: ListNode, P: ListNode):\n    \"\"\"在鏈結串列的節點 n0 之後插入節點 P\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
    linked_list.cpp
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.java
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.cs
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.go
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
    linked_list.swift
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
    linked_list.js
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.ts
    /* 在鏈結串列的節點 n0 之後插入節點 P */\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
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
    linked_list.rs
    /* 在鏈結串列的節點 n0 之後插入節點 P */\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
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.kt
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
    linked_list.rb
    ### 在鏈結串列的節點 n0 之後插入節點 _p ###\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-vs","level":2,"title":"4.2.2   陣列 vs. 鏈結串列","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":"

    單向鏈結串列通常用於實現堆疊、佇列、雜湊表和圖等資料結構。

    • 堆疊與佇列:當插入和刪除操作都在鏈結串列的一端進行時,它表現的特性為先進後出,對應堆疊;當插入操作在鏈結串列的一端進行,刪除操作在鏈結串列的另一端進行,它表現的特性為先進先出,對應佇列。
    • 雜湊表:鏈式位址是解決雜湊衝突的主流方案之一,在該方案中,所有衝突的元素都會被放到一個鏈結串列中。
    • 圖:鄰接表是表示圖的一種常用方式,其中圖的每個頂點都與一個鏈結串列相關聯,鏈結串列中的每個元素都代表與該頂點相連的其他頂點。

    雙向鏈結串列常用於需要快速查詢前一個和後一個元素的場景。

    • 高階資料結構:比如在紅黑樹、B 樹中,我們需要訪問節點的父節點,這可以透過在節點中儲存一個指向父節點的引用來實現,類似於雙向鏈結串列。
    • 瀏覽器歷史:在網頁瀏覽器中,當用戶點選前進或後退按鈕時,瀏覽器需要知道使用者訪問過的前一個和後一個網頁。雙向鏈結串列的特性使得這種操作變得簡單。
    • LRU 演算法:在快取淘汰(LRU)演算法中,我們需要快速找到最近最少使用的資料,以及支持快速新增和刪除節點。這時候使用雙向鏈結串列就非常合適。

    環形鏈結串列常用於需要週期性操作的場景,比如作業系統的資源排程。

    • 時間片輪轉排程演算法:在作業系統中,時間片輪轉排程演算法是一種常見的 CPU 排程演算法,它需要對一組程序進行迴圈。每個程序被賦予一個時間片,當時間片用完時,CPU 將切換到下一個程序。這種迴圈操作可以透過環形鏈結串列來實現。
    • 資料緩衝區:在某些資料緩衝區的實現中,也可能會使用環形鏈結串列。比如在音訊、影片播放器中,資料流可能會被分成多個緩衝塊並放入一個環形鏈結串列,以便實現無縫播放。
    ","path":["第 4 章   陣列與鏈結串列","4.2   鏈結串列"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   串列","text":"

    串列(list)是一個抽象的資料結構概念,它表示元素的有序集合,支持元素訪問、修改、新增、刪除和走訪等操作,無須使用者考慮容量限制的問題。串列可以基於鏈結串列或陣列實現。

    • 鏈結串列天然可以看作一個串列,其支持元素增刪查改操作,並且可以靈活動態擴容。
    • 陣列也支持元素增刪查改,但由於其長度不可變,因此只能看作一個具有長度限制的串列。

    當使用陣列實現串列時,長度不可變的性質會導致串列的實用性降低。這是因為我們通常無法事先確定需要儲存多少資料,從而難以選擇合適的串列長度。若長度過小,則很可能無法滿足使用需求;若長度過大,則會造成記憶體空間浪費。

    為解決此問題,我們可以使用動態陣列(dynamic array)來實現串列。它繼承了陣列的各項優點,並且可以在程式執行過程中進行動態擴容。

    實際上,許多程式語言中的標準庫提供的串列是基於動態陣列實現的,例如 Python 中的 list 、Java 中的 ArrayList 、C++ 中的 vector 和 C# 中的 List 等。在接下來的討論中,我們將把“串列”和“動態陣列”視為等同的概念。

    ","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// 有初始值(注意陣列的元素型別需為 int[] 的包裝類別 Integer[])\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
    視覺化執行

    全螢幕觀看 >

    ","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
    視覺化執行

    全螢幕觀看 >

    ","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)  # 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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:]...)...) // 在索引 3 處插入數字 6\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) // 在索引 3 處插入數字 6\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); // 在索引 3 處插入數字 6\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); // 在索引 3 處插入數字 6\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); // 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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) # 在索引 3 處插入數字 6\n\n# 刪除元素\nnums.delete_at(3) # 刪除索引 3 處的元素\n
    視覺化執行

    全螢幕觀看 >

    ","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
    視覺化執行

    全螢幕觀看 >

    ","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
    視覺化執行

    全螢幕觀看 >

    ","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
    視覺化執行

    全螢幕觀看 >

    ","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 提供高速的資料讀取,從而顯著提升程式的執行效率,減少對較慢的記憶體的依賴。

    圖 4-10   硬碟、記憶體和快取之間的資料流通

    ","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:為什麼陣列要求相同型別的元素,而在鏈結串列中卻沒有強調相同型別呢?

    鏈結串列由節點組成,節點之間透過引用(指標)連線,各個節點可以儲存不同型別的資料,例如 intdoublestringobject 等。

    相對地,陣列元素則必須是相同型別的,這樣才能透過計算偏移量來獲取對應元素位置。例如,陣列同時包含 intlong 兩種型別,單個元素分別佔用 4 位元組和 8 位元組 ,此時就不能用以下公式計算偏移量了,因為陣列中包含了兩種“元素長度”。

    # 元素記憶體位址 = 陣列記憶體位址(首元素記憶體位址) + 元素長度 * 元素索引\n

    Q:刪除節點 P 後,是否需要把 P.next 設為 None 呢?

    不修改 P.next 也可以。從該鏈結串列的角度看,從頭節點走訪到尾節點已經不會遇到 P 了。這意味著節點 P 已經從鏈結串列中刪除了,此時節點 P 指向哪裡都不會對該鏈結串列產生影響。

    從資料結構與演算法(做題)的角度看,不斷開沒有關係,只要保證程式的邏輯是正確的就行。從標準庫的角度看,斷開更加安全、邏輯更加清晰。如果不斷開,假設被刪除節點未被正常回收,那麼它會影響後繼節點的記憶體回收。

    Q:在鏈結串列中插入和刪除操作的時間複雜度是 \\(O(1)\\) 。但是增刪之前都需要 \\(O(n)\\) 的時間查詢元素,那為什麼時間複雜度不是 \\(O(n)\\) 呢?

    如果是先查詢元素、再刪除元素,時間複雜度確實是 \\(O(n)\\) 。然而,鏈結串列的 \\(O(1)\\) 增刪的優勢可以在其他應用上得到體現。例如,雙向佇列適合使用鏈結串列實現,我們維護一個指標變數始終指向頭節點、尾節點,每次插入與刪除操作都是 \\(O(1)\\) 。

    Q:圖“鏈結串列定義與儲存方式”中,淺藍色的儲存節點指標是佔用一塊記憶體位址嗎?還是和節點值各佔一半呢?

    該示意圖只是定性表示,定量表示需要根據具體情況進行分析。

    • 不同型別的節點值佔用的空間是不同的,比如 intlongdouble 和例項物件等。
    • 指標變數佔用的記憶體空間大小根據所使用的作業系統及編譯環境而定,大多為 8 位元組或 4 位元組。

    Q:在串列末尾新增元素是否時時刻刻都為 \\(O(1)\\) ?

    如果新增元素時超出串列長度,則需要先擴容串列再新增。系統會申請一塊新的記憶體,並將原串列的所有元素搬運過去,這時候時間複雜度就會是 \\(O(n)\\) 。

    Q:“串列的出現極大地提高了陣列的實用性,但可能導致部分記憶體空間浪費”,這裡的空間浪費是指額外增加的變數如容量、長度、擴容倍數所佔的記憶體嗎?

    這裡的空間浪費主要有兩方面含義:一方面,串列都會設定一個初始長度,我們不一定需要用這麼多;另一方面,為了防止頻繁擴容,擴容一般會乘以一個係數,比如 \\(\\times 1.5\\) 。這樣一來,也會出現很多空位,我們通常不能完全填滿它們。

    Q:在 Python 中初始化 n = [1, 2, 3] 後,這 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 的效能會更好。

    另一方面,必要使用鏈結串列的情況主要是二元樹和圖。堆疊和佇列往往會使用程式語言提供的 stackqueue ,而非鏈結串列。

    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 章   回溯"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"本章內容","text":"
    • 13.1   回溯演算法
    • 13.2   全排列問題
    • 13.3   子集和問題
    • 13.4   N 皇后問題
    • 13.5   小結
    ","path":["第 13 章   回溯"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   回溯演算法","text":"

    回溯演算法(backtracking algorithm)是一種透過窮舉來解決問題的方法,它的核心思想是從一個初始狀態出發,暴力搜尋所有可能的解決方案,當遇到正確的解則將其記錄,直到找到解或者嘗試了所有可能的選擇都無法找到解為止。

    回溯演算法通常採用“深度優先搜尋”來走訪解空間。在“二元樹”章節中,我們提到前序、中序和後序走訪都屬於深度優先搜尋。接下來,我們利用前序走訪構造一個回溯問題,逐步瞭解回溯演算法的工作原理。

    例題一

    給定一棵二元樹,搜尋並記錄所有值為 \\(7\\) 的節點,請返回節點串列。

    對於此題,我們前序走訪這棵樹,並判斷當前節點的值是否為 \\(7\\) ,若是,則將該節點的值加入結果串列 res 之中。相關過程實現如圖 13-1 和以下程式碼所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序走訪:例題一\"\"\"\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    ### 前序走訪:例題一 ###\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":"

    之所以稱之為回溯演算法,是因為該演算法在搜尋解空間時會採用“嘗試”與“回退”的策略。當演算法在搜尋過程中遇到某個狀態無法繼續前進或無法得到滿足條件的解時,它會撤銷上一步的選擇,退回到之前的狀態,並嘗試其他可能的選擇。

    對於例題一,訪問每個節點都代表一次“嘗試”,而越過葉節點或返回父節點的 return 則表示“回退”。

    值得說明的是,回退並不僅僅包括函式返回。為解釋這一點,我們對例題一稍作拓展。

    例題二

    在二元樹中搜索所有值為 \\(7\\) 的節點,請返回根節點到這些節點的路徑。

    在例題一程式碼的基礎上,我們需要藉助一個串列 path 記錄訪問過的節點路徑。當訪問到值為 \\(7\\) 的節點時,則複製 path 並新增進結果串列 res 。走訪完成後,res 中儲存的就是所有的解。程式碼如下所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序走訪:例題二\"\"\"\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    ### 前序走訪:例題二 ###\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":"

    複雜的回溯問題通常包含一個或多個約束條件,約束條件通常可用於“剪枝”。

    例題三

    在二元樹中搜索所有值為 \\(7\\) 的節點,請返回根節點到這些節點的路徑,並要求路徑中不包含值為 \\(3\\) 的節點。

    為了滿足以上約束條件,我們需要新增剪枝操作:在搜尋過程中,若遇到值為 \\(3\\) 的節點,則提前返回,不再繼續搜尋。程式碼如下所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序走訪:例題三\"\"\"\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    ### 前序走訪:例題三 ###\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

    接下來,我們基於框架程式碼來解決例題三。狀態 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    \"\"\"回溯演算法:例題三\"\"\"\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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### 回溯演算法:例題三 ###\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 的搜尋過程對比

    相比基於前序走訪的程式碼實現,基於回溯演算法框架的程式碼實現雖然顯得囉唆,但通用性更好。實際上,許多回溯問題可以在該框架下解決。我們只需根據具體問題來定義 statechoices ,並實現框架中的各個方法即可。

    ","path":["第 13 章   回溯","13.1   回溯演算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   常用術語","text":"

    為了更清晰地分析演算法問題,我們總結一下回溯演算法中常用術語的含義,並對照例題三給出對應示例,如表 13-1 所示。

    表 13-1   常見的回溯演算法術語

    名詞 定義 例題三 解(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 \\times n\\) 的棋盤上放置 \\(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":"

    為了滿足列約束,我們可以利用一個長度為 \\(n\\) 的布林型陣列 cols 記錄每一列是否有皇后。在每次決定放置前,我們透過 cols 將已有皇后的列進行剪枝,並在回溯中動態更新 cols 的狀態。

    Tip

    請注意,矩陣的起點位於左上角,其中行索引從上到下增加,列索引從左到右增加。

    那麼,如何處理對角線約束呢?設棋盤中某個格子的行列索引為 \\((row, col)\\) ,選定矩陣中的某條主對角線,我們發現該對角線上所有格子的行索引減列索引都相等,即主對角線上所有格子的 \\(row - col\\) 為恆定值。

    也就是說,如果兩個格子滿足 \\(row_1 - col_1 = row_2 - col_2\\) ,則它們一定處在同一條主對角線上。利用該規律,我們可以藉助圖 13-18 所示的陣列 diags1 記錄每條主對角線上是否有皇后。

    同理,次對角線上的所有格子的 \\(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\\) ,即陣列 diags1diags2 的長度都為 \\(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)\\) 空間,陣列 colsdiags1diags2 皆使用 \\(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\\) ,我們將第二個 \\(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":"

    請注意,雖然 selectedduplicated 都用於剪枝,但兩者的目標不同。

    • 重複選擇剪枝:整個搜尋過程中只有一個 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; // 初始化解的數量為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 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    # 剪枝二:從 start 開始走訪,避免生成重複子集\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 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  // 剪枝二:從 start 開始走訪,避免生成重複子集\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超過 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  # 剪枝二:從 start 開始走訪,避免生成重複子集\n  for i in start...choices.length\n    # 剪枝一:若子集和超過 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    # 剪枝二:從 start 開始走訪,避免生成重複子集\n    # 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超過 target ,則直接結束迴圈\n        # 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if target - choices[i] < 0:\n            break\n        # 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if target - choices[i] < 0 {\n            break\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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  // 剪枝二:從 start 開始走訪,避免生成重複子集\n  // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超過 target ,則直接結束迴圈\n    // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超過 target ,則直接跳過\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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  # 剪枝二:從 start 開始走訪,避免生成重複子集\n  # 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n  for i in start...choices.length\n    # 剪枝一:若子集和超過 target ,則直接結束迴圈\n    # 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n    break if target - choices[i] < 0\n    # 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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-q-a","level":3,"title":"2.   Q & A","text":"

    Q:怎麼理解回溯和遞迴的關係?

    總的來看,回溯是一種“演算法策略”,而遞迴更像是一個“工具”。

    • 回溯演算法通常基於遞迴實現。然而,回溯是遞迴的應用場景之一,是遞迴在搜尋問題中的應用。
    • 遞迴的結構體現了“子問題分解”的解題範式,常用於解決分治、回溯、動態規劃(記憶化遞迴)等問題。
    ","path":["第 13 章   回溯","13.5   小結"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章   複雜度分析","text":"

    Abstract

    複雜度分析猶如浩瀚的演算法宇宙中的時空嚮導。

    它帶領我們在時間與空間這兩個維度上深入探索,尋找更優雅的解決方案。

    ","path":["第 2 章   複雜度分析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"本章內容","text":"
    • 2.1   演算法效率評估
    • 2.2   迭代與遞迴
    • 2.3   時間複雜度
    • 2.4   空間複雜度
    • 2.5   小結
    ","path":["第 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 迴圈是最常見的迭代形式之一,適合在預先知道迭代次數時使用。

    以下函式基於 for 迴圈實現了求和 \\(1 + 2 + \\dots + n\\) ,求和結果使用變數 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 迴圈中,程式每輪都會先檢查條件,如果條件為真,則繼續執行,否則就結束迴圈。

    下面我們用 while 迴圈來實現求和 \\(1 + 2 + \\dots + n\\) :

    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_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 所示,這樣不斷遞迴呼叫下去,最終將產生一棵層數為 \\(n\\) 的遞迴樹(recursion tree)。

    圖 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() 在迴圈中呼叫了 \\(n\\) 次 function() ,每輪中的 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_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_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 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":"

    演算法效率評估

    • 時間效率和空間效率是衡量演算法優劣的兩個主要評價指標。
    • 我們可以透過實際測試來評估演算法效率,但難以消除測試環境的影響,且會耗費大量計算資源。
    • 複雜度分析可以消除實際測試的弊端,分析結果適用於所有執行平臺,並且能夠揭示演算法在不同資料規模下的效率。

    時間複雜度

    • 時間複雜度用於衡量演算法執行時間隨資料量增長的趨勢,可以有效評估演算法效率,但在某些情況下可能失效,如在輸入的資料量較小或時間複雜度相同時,無法精確對比演算法效率的優劣。
    • 最差時間複雜度使用大 \\(O\\) 符號表示,對應函式漸近上界,反映當 \\(n\\) 趨向正無窮時,操作數量 \\(T(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)與一個物件關聯,被隱式傳遞給呼叫它的物件,能夠對類別的例項中包含的資料進行操作。

    下面以幾種常見的程式語言為例來說明。

    • C 語言是程序式程式設計語言,沒有物件導向的概念,所以只有函式。但我們可以透過建立結構體(struct)來模擬物件導向程式設計,與結構體相關聯的函式就相當於其他程式語言中的方法。
    • Java 和 C# 是物件導向的程式語言,程式碼塊(方法)通常作為某個類別的一部分。靜態方法的行為類似於函式,因為它被繫結在類別上,不能訪問特定的例項變數。
    • C++ 和 Python 既支持程序式程式設計(函式),也支持物件導向程式設計(方法)。

    Q:圖解“常見的空間複雜度型別”反映的是否是佔用空間的絕對大小?

    不是,該圖展示的是空間複雜度,其反映的是增長趨勢,而不是佔用空間的絕對大小。

    假設取 \\(n = 8\\) ,你可能會發現每條曲線的值與函式對應不上。這是因為每條曲線都包含一個常數項,用於將取值範圍壓縮到一個視覺舒適的範圍內。

    在實際中,因為我們通常不知道每個方法的“常數項”複雜度是多少,所以一般無法僅憑複雜度來選擇 \\(n = 8\\) 之下的最優解法。但對於 \\(n = 8^5\\) 就很好選了,這時增長趨勢已經佔主導了。

    Q 是否存在根據實際使用場景,選擇犧牲時間(或空間)來設計演算法的情況?

    在實際應用中,大部分情況會選擇犧牲空間換時間。例如資料庫索引,我們通常選擇建立 B+ 樹或雜湊索引,佔用大量記憶體空間,以換取 \\(O(\\log n)\\) 甚至 \\(O(1)\\) 的高效查詢。

    在空間資源寶貴的場景,也會選擇犧牲時間換空間。例如在嵌入式開發中,裝置記憶體很寶貴,工程師可能會放棄使用雜湊表,選擇使用陣列順序查詢,以節省記憶體佔用,代價是查詢變慢。

    ","path":["第 2 章   複雜度分析","2.5   小結"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   時間複雜度","text":"

    執行時間可以直觀且準確地反映演算法的效率。如果我們想準確預估一段程式碼的執行時間,應該如何操作呢?

    1. 確定執行平臺,包括硬體配置、程式語言、系統環境等,這些因素都會影響程式碼的執行效率。
    2. 評估各種計算操作所需的執行時間,例如加法操作 + 需要 1 ns ,乘法操作 * 需要 10 ns ,列印操作 print() 需要 5 ns 等。
    3. 統計程式碼中所有的計算操作,並將所有操作的執行時間求和,從而得到執行時間。

    例如在以下程式碼中,輸入資料大小為 \\(n\\) :

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # 在某執行平臺下\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # 迴圈 n 次\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
    // 在某執行平臺下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 迴圈 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
    // 在某執行平臺下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 迴圈 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
    // 在某執行平臺下\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 迴圈 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // 迴圈 n 次\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 迴圈 n 次\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 迴圈 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 迴圈 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // 在某執行平臺下\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // 迴圈 n 次\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
    // 在某執行平臺下\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // 迴圈 n 次\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
    // 在某執行平臺下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 迴圈 n 次\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 迴圈 n 次\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
    # 在某執行平臺下\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # 迴圈 n 次\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

    根據以上方法,可以得到演算法的執行時間為 \\((6n + 12)\\) ns :

    \\[ 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\\) ,給定三個演算法 ABC

    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 只有 \\(1\\) 個列印操作,演算法執行時間不隨著 \\(n\\) 增大而增長。我們稱此演算法的時間複雜度為“常數階”。
    • 演算法 B 中的列印操作需要迴圈 \\(n\\) 次,演算法執行時間隨著 \\(n\\) 增大呈線性增長。此演算法的時間複雜度被稱為“線性階”。
    • 演算法 C 中的列印操作需要迴圈 \\(1000000\\) 次,雖然執行時間很長,但它與輸入資料大小 \\(n\\) 無關。因此 C 的時間複雜度和 A 相同,仍為“常數階”。

    圖 2-7   演算法 A、B 和 C 的時間增長趨勢

    相較於直接統計演算法的執行時間,時間複雜度分析有哪些特點呢?

    • 時間複雜度能夠有效評估演算法效率。例如,演算法 B 的執行時間呈線性增長,在 \\(n > 1\\) 時比演算法 A 更慢,在 \\(n > 1000000\\) 時比演算法 C 更慢。事實上,只要輸入資料大小 \\(n\\) 足夠大,複雜度為“常數階”的演算法一定優於“線性階”的演算法,這正是時間增長趨勢的含義。
    • 時間複雜度的推算方法更簡便。顯然,執行平臺和計算操作型別都與演算法執行時間的增長趨勢無關。因此在時間複雜度分析中,我們可以簡單地將所有計算操作的執行時間視為相同的“單位時間”,從而將“計算操作執行時間統計”簡化為“計算操作數量統計”,這樣一來估算難度就大大降低了。
    • 時間複雜度也存在一定的侷限性。例如,儘管演算法 AC 的時間複雜度相同,但實際執行時間差別很大。同樣,儘管演算法 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)\\) ,這個數學符號稱為大 \\(O\\) 記號(big-\\(O\\) notation),表示函式 \\(T(n)\\) 的漸近上界(asymptotic upper bound)。

    時間複雜度分析本質上是計算“操作數量 \\(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","level":3,"title":"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","level":3,"title":"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 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\\) 為輸入資料大小;在第二個示例中,陣列長度 \\(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 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":"

    生物學的“細胞分裂”是指數階增長的典型例子:初始狀態為 \\(1\\) 個細胞,分裂一輪後變為 \\(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 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 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 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 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    # 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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  // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 n 個\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
    time_complexity.rb
    ### 階乘階(遞迴實現)###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # 從 1 個分裂出 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":"

    演算法的時間效率往往不是固定的,而是與輸入資料的分佈有關。假設輸入一個長度為 \\(n\\) 的陣列 nums ,其中 nums 由從 \\(1\\) 至 \\(n\\) 的數字組成,每個數字只出現一次;但元素順序是隨機打亂的,任務目標是返回元素 \\(1\\) 的索引。我們可以得出以下結論。

    • nums = [?, ?, ..., 1] ,即當末尾元素是 \\(1\\) 時,需要完整走訪陣列,達到最差時間複雜度 \\(O(n)\\) 。
    • nums = [1, ?, ?, ...] ,即當首個元素為 \\(1\\) 時,無論陣列多長都不需要繼續走訪,達到最佳時間複雜度 \\(\\Omega(1)\\) 。

    “最差時間複雜度”對應函式漸近上界,使用大 \\(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    \"\"\"查詢陣列 nums 中數字 1 所在索引\"\"\"\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    // 使用系統時間生成隨機種子\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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### 查詢陣列 nums 中數字 1 所在索引 ###\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 章   資料結構"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"本章內容","text":"
    • 3.1   資料結構分類
    • 3.2   基本資料型別
    • 3.3   數字編碼 *
    • 3.4   字元編碼 *
    • 3.5   小結
    ","path":["第 3 章   資料結構"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   基本資料型別","text":"

    當談及計算機中的資料時,我們會想到文字、圖片、影片、語音、3D 模型等各種形式。儘管這些資料的組織形式各異,但它們都由各種基本資料型別構成。

    基本資料型別是 CPU 可以直接進行運算的型別,在演算法中直接被使用,主要包括以下幾種。

    • 整數型別 byteshortintlong
    • 浮點數型別 floatdouble ,用於表示小數。
    • 字元型別 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 型別,單個字元實際上是長度為 1 的字串 str
    • C 和 C++ 未明確規定基本資料型別的大小,而因實現和平臺各異。表 3-1 遵循 LP64 資料模型,其用於包括 Linux 和 macOS 在內的 Unix 64 位作業系統。
    • 字元 char 的大小在 C 和 C++ 中為 1 位元組,在大多數程式語言中取決於特定的字元編碼方法,詳見“字元編碼”章節。
    • 即使表示布林量僅需 1 位(\\(0\\) 或 \\(1\\)),它在記憶體中通常也儲存為 1 位元組。這是因為現代計算機 CPU 通常將 1 位元組作為最小定址記憶體單元。

    那麼,基本資料型別與資料結構之間有什麼關聯呢?我們知道,資料結構是在計算機中組織與儲存資料的方式。這句話的主語是“結構”而非“資料”。

    如果想表示“一排數字”,我們自然會想到使用陣列。這是因為陣列的線性結構可以表示數字的相鄰關係和順序關係,但至於儲存的內容是整數 int、小數 float 還是字元 char ,則與“資料結構”無關。

    換句話說,基本資料型別提供了資料的“內容型別”,而資料結構提供了資料的“組織方式”。例如以下程式碼,我們用相同的資料結構(陣列)來儲存與表示不同的基本資料型別,包括 intfloatcharbool 等。

    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
    視覺化執行

    全螢幕觀看 >

    ","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 字元集。它在 ASCII 的 7 位基礎上擴展到 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 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字元集之中,提供一種通用的字元集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。

    自 1991 年釋出以來,Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月,Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。在龐大的 Unicode 字元集中,常用的字元佔用 2 位元組,有些生僻的字元佔用 3 位元組甚至 4 位元組。

    Unicode 是一種通用字元集,本質上是給每個字元分配一個編號(稱為“碼點”),但它並沒有規定在計算機中如何儲存這些字元碼點。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元?

    對於以上問題,一種直接的解決方案是將所有字元儲存為等長的編碼。如圖 3-7 所示,“Hello”中的每個字元佔用 1 位元組,“演算法”中的每個字元佔用 2 位元組。我們可以透過高位填 0 將“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 字元在 Unicode 字元集中佔據了前 128 個碼點。也就是說,UTF-8 編碼可以向下相容 ASCII 碼。這意味著我們可以使用 UTF-8 來解析年代久遠的 ASCII 碼文字。
    • 對於長度為 \\(n\\) 位元組的字元(其中 \\(n > 1\\)),將首個位元組的高 \\(n\\) 位都設定為 \\(1\\) ,第 \\(n + 1\\) 位設定為 \\(0\\) ;從第二個位元組開始,將每個位元組的高 2 位都設定為 \\(10\\) ;其餘所有位用於填充字元的 Unicode 碼點。

    圖 3-8 展示了“Hello演算法”對應的 UTF-8 編碼。觀察發現,由於最高 \\(n\\) 位都設定為 \\(1\\) ,因此系統可以透過讀取最高位 \\(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\\) 個字元,我們需要從字串的開始處走訪到第 \\(i\\) 個字元,這需要 \\(O(n)\\) 的時間。
    • 字元計數:與隨機訪問類似,計算 UTF-16 編碼的字串的長度也是 \\(O(1)\\) 的操作。但是,計算 UTF-8 編碼的字串的長度需要走訪整個字串。
    • 字串操作:在 UTF-16 編碼的字串上,很多字串操作(如分割、連線、插入、刪除等)更容易進行。在 UTF-8 編碼的字串上,進行這些操作通常需要額外的計算,以確保不會產生無效的 UTF-8 編碼。

    實際上,程式語言的字元編碼方案設計是一個很有趣的話題,涉及許多因素。

    • Java 的 String 型別使用 UTF-16 編碼,每個字元佔用 2 位元組。這是因為 Java 語言設計之初,人們認為 16 位足以表示所有可能的字元。然而,這是一個不正確的判斷。後來 Unicode 規範擴展到了超過 16 位,所以 Java 中的字元現在可能由一對 16 位的值(稱為“代理對”)表示。
    • JavaScript 和 TypeScript 的字串使用 UTF-16 編碼的原因與 Java 類似。當 1995 年 Netscape 公司首次推出 JavaScript 語言時,Unicode 還處於發展早期,那時候使用 16 位的編碼就足以表示所有的 Unicode 字元了。
    • C# 使用 UTF-16 編碼,主要是因為 .NET 平臺是由 Microsoft 設計的,而 Microsoft 的很多技術(包括 Windows 作業系統)都廣泛使用 UTF-16 編碼。

    由於以上程式語言對字元數量的低估,它們不得不採取“代理對”的方式來表示超過 16 位長度的 Unicode 字元。這是一個不得已為之的無奈之舉。一方面,包含代理對的字串中,一個字元可能佔用 2 位元組或 4 位元組,從而喪失了等長編碼的優勢。另一方面,處理代理對需要額外增加程式碼,這提高了程式設計的複雜性和除錯難度。

    出於以上原因,部分程式語言提出了一些不同的編碼方案。

    • Python 中的 str 使用 Unicode 編碼,並採用一種靈活的字串表示,儲存的字元長度取決於字串中最大的 Unicode 碼點。若字串中全部是 ASCII 字元,則每個字元佔用 1 位元組;如果有字元超出了 ASCII 範圍,但全部在基本多語言平面(BMP)內,則每個字元佔用 2 位元組;如果有超出 BMP 的字元,則每個字元佔用 4 位元組。
    • Go 語言的 string 型別在內部使用 UTF-8 編碼。Go 語言還提供了 rune 型別,它用於表示單個 Unicode 碼點。
    • Rust 語言的 strString 型別在內部使用 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 位的 \\(1\\) 會被捨棄。也就是說,負零的二補數為 \\(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":"

    細心的你可能會發現:intfloat 長度相同,都是 4 位元組 ,但為什麼 float 的取值範圍遠大於 int ?這非常反直覺,因為按理說 float 需要表示小數,取值範圍應該變小才對。

    實際上,這是因為浮點數 float 採用了不同的表示方式。記一個 32 位元長度的二進位制數為:

    \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

    根據 IEEE 754 標準,32-bit 長度的 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   IEEE 754 標準下的 float 的計算示例

    觀察圖 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":"
    • 資料結構可以從邏輯結構和物理結構兩個角度進行分類。邏輯結構描述了資料元素之間的邏輯關係,而物理結構描述了資料在計算機記憶體中的儲存方式。
    • 常見的邏輯結構包括線性、樹狀和網狀等。通常我們根據邏輯結構將資料結構分為線性(陣列、鏈結串列、堆疊、佇列)和非線性(樹、圖、堆積)兩種。雜湊表的實現可能同時包含線性資料結構和非線性資料結構。
    • 當程式執行時,資料被儲存在計算機記憶體中。每個記憶體空間都擁有對應的記憶體位址,程式透過這些記憶體位址訪問資料。
    • 物理結構主要分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。所有資料結構都是由陣列、鏈結串列或兩者的組合實現的。
    • 計算機中的基本資料型別包括整數 byteshortintlong ,浮點數 floatdouble ,字元 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:基於陣列實現的資料結構也稱“靜態資料結構” 是否有歧義?堆疊也可以進行出堆疊和入堆疊等操作,這些操作都是“動態”的。

    堆疊確實可以實現動態的資料操作,但資料結構仍然是“靜態”(長度不可變)的。儘管基於陣列的資料結構可以動態地新增或刪除元素,但它們的容量是固定的。如果資料量超出了預分配的大小,就需要建立一個新的更大的陣列,並將舊陣列的內容複製到新陣列中。

    Q:在構建堆疊(佇列)的時候,未指定它的大小,為什麼它們是“靜態資料結構”呢?

    在高階程式語言中,我們無須人工指定堆疊(佇列)的初始容量,這個工作由類別內部自動完成。例如,Java 的 ArrayList 的初始容量通常為 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 章   分治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"本章內容","text":"
    • 12.1   分治演算法
    • 12.2   分治搜尋策略
    • 12.3   構建樹問題
    • 12.4   河內塔問題
    • 12.5   小結
    ","path":["第 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

    給定一個長度為 \\(n\\) 的有序陣列 nums ,其中所有元素都是唯一的,請查詢元素 target

    從分治角度,我們將搜尋區間 \\([i, j]\\) 對應的子問題記為 \\(f(i, j)\\) 。

    以原問題 \\(f(0, n-1)\\) 為起始點,透過以下步驟進行二分搜尋。

    1. 計算搜尋區間 \\([i, j]\\) 的中點 \\(m\\) ,根據它排除一半搜尋區間。
    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":"

    原問題定義為從 preorderinorder 構建二元樹,是一個典型的分治問題。

    • 問題可以分解:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構建左子樹、構建右子樹,加上一步操作:初始化根節點。而對於每棵子樹(子問題),我們仍然可以複用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
    • 子問題是獨立的:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構建左子樹時,我們只需關注中序走訪和前序走訪中與左子樹對應的部分。右子樹同理。
    • 子問題的解可以合併:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們連結到根節點上,得到原問題的解。
    ","path":["第 12 章   分治","12.3   構建二元樹問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   如何劃分子樹","text":"

    根據以上分析,這道題可以使用分治來求解,但如何透過前序走訪 preorder 和中序走訪 inorder 來劃分左子樹和右子樹呢?

    根據定義,preorderinorder 都可以劃分為三個部分。

    • 前序走訪:[ 根節點 | 左子樹 | 右子樹 ] ,例如圖 12-5 的樹對應 [ 3 | 9 | 2 1 7 ]
    • 中序走訪:[ 左子樹 | 根節點 | 右子樹 ] ,例如圖 12-5 的樹對應 [ 9 | 3 | 1 2 7 ]

    以上圖資料為例,我們可以透過圖 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":"

    根據以上劃分方法,我們已經得到根節點、左子樹、右子樹在 preorderinorder 中的索引區間。而為了描述這些索引區間,我們需要藉助幾個指標變數。

    • 將當前樹的根節點在 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":"

    一方面,分治可以用來解決許多經典演算法問題。

    • 尋找最近點對:該演算法首先將點集分成兩部分,然後分別找出兩部分中的最近點對,最後找出跨越兩部分的最近點對。
    • 大整數乘法:例如 Karatsuba 演算法,它將大整數乘法分解為幾個較小的整數的乘法和加法。
    • 矩陣乘法:例如 Strassen 演算法,它將大矩陣乘法分解為多個小矩陣的乘法和加法。
    • 河內塔問題:河內塔問題可以透過遞迴解決,這是典型的分治策略應用。
    • 求解逆序對:在一個序列中,如果前面的數字大於後面的數字,那麼這兩個數字構成一個逆序對。求解逆序對問題可以利用分治的思想,藉助合併排序進行求解。

    另一方面,分治在演算法和資料結構的設計中應用得非常廣泛。

    • 二分搜尋:二分搜尋是將有序陣列從中點索引處分為兩部分,然後根據目標值與中間元素值比較結果,決定排除哪一半區間,並在剩餘區間執行相同的二分操作。
    • 合併排序:本節開頭已介紹,不再贅述。
    • 快速排序:快速排序是選取一個基準值,然後把陣列分為兩個子陣列,一個子陣列的元素比基準值小,另一子陣列的元素比基準值大,再對這兩部分進行相同的劃分操作,直至子陣列只剩下一個元素。
    • 桶排序:桶排序的基本思想是將資料分散到多個桶,然後對每個桶內的元素進行排序,最後將各個桶的元素依次取出,從而得到一個有序陣列。
    • 樹:例如二元搜尋樹、AVL 樹、紅黑樹、B 樹、B+ 樹等,它們的查詢、插入和刪除等操作都可以視為分治策略的應用。
    • 堆積:堆積是一種特殊的完全二元樹,其各種操作,如插入、刪除和堆積化,實際上都隱含了分治的思想。
    • 雜湊表:雖然雜湊表並不直接應用分治,但某些雜湊衝突解決方案間接應用了分治策略,例如,鏈式位址中的長鏈結串列會被轉化為紅黑樹,以提升查詢效率。

    可以看出,分治是一種“潤物細無聲”的演算法思想,隱含在各種演算法與資料結構之中。

    ","path":["第 12 章   分治","12.1   分治演算法"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   河內塔問題","text":"

    在合併排序和構建二元樹中,我們都是將原問題分解為兩個規模為原問題一半的子問題。然而對於河內塔問題,我們採用不同的分解策略。

    Question

    給定三根柱子,記為 ABC 。起始狀態下,柱子 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)\\) 的過程可總結為:將兩個圓盤藉助 BA 移至 C 。其中,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 中剩餘的一個圓盤從 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\\) 個圓盤藉助 CA 移至 B
    2. 將剩餘 \\(1\\) 個圓盤從 A 直接移至 C
    3. 將 \\(n-1\\) 個圓盤藉助 AB 移至 C

    對於這兩個子問題 \\(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) ,它的作用是將柱 src 頂部的 \\(i\\) 個圓盤藉助緩衝柱 buf 移動至目標柱 tar

    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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    # 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar)\n    # 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    # 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    DFS(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    Move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfsHanota(i-1, src, tar, buf)\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar)\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* 求解河內塔問題 */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\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) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 src 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\n    dfs(i - 1, buf, src, tar);\n}\n\n/* 求解河內塔問題 */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n  dfs(i - 1, src, tar, buf);\n  // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n  move(src, tar);\n  // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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  // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move_pan(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\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) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar)\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n  dfs(i - 1, src, tar, buf)\n  # 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n  move(src, tar)\n  # 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\n  dfs(i - 1, buf, src, tar)\nend\n\n### 求解河內塔問題 ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # 將 A 頂部 n 個圓盤藉助 B 移到 C\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}\\) 秒,合約 \\(5850\\) 億年,遠遠超過了現在對宇宙年齡的估計。所以,倘若這個傳說是真的,我們應該不需要擔心世界末日的到來。

    ","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 章   動態規劃"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"本章內容","text":"
    • 14.1   初探動態規劃
    • 14.2   DP 問題特性
    • 14.3   DP 解題思路
    • 14.4   0-1 背包問題
    • 14.5   完全背包問題
    • 14.6   編輯距離問題
    • 14.7   小結
    ","path":["第 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\\) ,則從地面爬到第 \\(3\\) 階的最小代價為 \\(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 所示,爬上第 \\(3\\) 階僅剩 \\(2\\) 種可行方案,其中連續三次跳 \\(1\\) 階的方案不滿足約束條件,因此被捨棄。

    圖 14-8   帶約束爬到第 3 階的方案數量

    在該問題中,如果上一輪是跳 \\(1\\) 階上來的,那麼下一輪就必須跳 \\(2\\) 階。這意味著,下一步選擇不能由當前狀態(當前所在樓梯階數)獨立決定,還和前一個狀態(上一輪所在樓梯階數)有關。

    不難發現,此問題已不滿足無後效性,狀態轉移方程 \\(dp[i] = dp[i-1] + dp[i-2]\\) 也失效了,因為 \\(dp[i-1]\\) 代表本輪跳 \\(1\\) 階,但其中包含了許多“上一輪是跳 \\(1\\) 階上來的”方案,而為了滿足約束,我們就不能將 \\(dp[i-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

    給定一個 \\(n \\times m\\) 的二維網格 grid ,網格中的每個單元格包含一個非負整數,表示該單元格的代價。機器人以左上角單元格為起始點,每次只能向下或者向右移動一步,直至到達右下角單元格。請返回從左上角到右下角的最小路徑和。

    圖 14-10 展示了一個例子,給定網格的最小路徑和為 \\(13\\) 。

    圖 14-10   最小路徑和示例資料

    第一步:思考每輪的決策,定義狀態,從而得到 \\(dp\\) 表

    本題的每一輪的決策就是從當前格子向下或向右走一步。設當前格子的行列索引為 \\([i, j]\\) ,則向下或向右走一步後,索引變為 \\([i+1, j]\\) 或 \\([i, j+1]\\) 。因此,狀態應包含行索引和列索引兩個變數,記為 \\([i, j]\\) 。

    狀態 \\([i, j]\\) 對應的子問題為:從起始點 \\([0, 0]\\) 走到 \\([i, j]\\) 的最小路徑和,解記為 \\(dp[i, j]\\) 。

    至此,我們就得到了圖 14-11 所示的二維 \\(dp\\) 矩陣,其尺寸與輸入網格 \\(grid\\) 相同。

    圖 14-11   狀態定義與 dp 表

    Note

    動態規劃和回溯過程可以描述為一個決策序列,而狀態由所有決策變數構成。它應當包含描述解題進度的所有變數,其包含了足夠的資訊,能夠用來推導出下一個狀態。

    每個狀態都對應一個子問題,我們會定義一個 \\(dp\\) 表來儲存所有子問題的解,狀態的每個獨立變數都是 \\(dp\\) 表的一個維度。從本質上看,\\(dp\\) 表是狀態和子問題的解之間的對映。

    第二步:找出最優子結構,進而推導出狀態轉移方程

    對於狀態 \\([i, j]\\) ,它只能從上邊格子 \\([i-1, j]\\) 和左邊格子 \\([i, j-1]\\) 轉移而來。因此最優子結構為:到達 \\([i, j]\\) 的最小路徑和由 \\([i, j-1]\\) 的最小路徑和與 \\([i-1, j]\\) 的最小路徑和中較小的那一個決定。

    根據以上分析,可推出圖 14-12 所示的狀態轉移方程:

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

    圖 14-12   最優子結構與狀態轉移方程

    Note

    根據定義好的 \\(dp\\) 表,思考原問題和子問題的關係,找出透過子問題的最優解來構造原問題的最優解的方法,即最優子結構。

    一旦我們找到了最優子結構,就可以使用它來構建出狀態轉移方程。

    第三步:確定邊界條件和狀態轉移順序

    在本題中,處在首行的狀態只能從其左邊的狀態得來,處在首列的狀態只能從其上邊的狀態得來,因此首行 \\(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","level":3,"title":"1.   方法一:暴力搜尋","text":"

    從狀態 \\([i, j]\\) 開始搜尋,不斷分解為更小的狀態 \\([i-1, j]\\) 和 \\([i, j-1]\\) ,遞迴函式包括以下要素。

    • 遞迴參數:狀態 \\([i, j]\\) 。
    • 返回值:從 \\([0, 0]\\) 到 \\([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","level":3,"title":"2.   方法二:記憶化搜尋","text":"

    我們引入一個和網格 grid 相同尺寸的記憶串列 mem ,用於記錄各個子問題的解,並將重疊子問題進行剪枝:

    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","level":3,"title":"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":"

    編輯距離,也稱 Levenshtein 距離,指兩個字串之間互相轉換的最少修改次數,通常用於在資訊檢索和自然語言處理中度量兩個序列的相似度。

    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":"

    第一步:思考每輪的決策,定義狀態,從而得到 \\(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, j]\\) 對應的子問題:將 \\(s\\) 的前 \\(i\\) 個字元更改為 \\(t\\) 的前 \\(j\\) 個字元所需的最少編輯步數。

    至此,得到一個尺寸為 \\((i+1) \\times (j+1)\\) 的二維 \\(dp\\) 表。

    第二步:找出最優子結構,進而推導出狀態轉移方程

    考慮子問題 \\(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] \\]

    第三步:確定邊界條件和狀態轉移順序

    當兩字串都為空時,編輯步數為 \\(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-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","level":2,"title":"14.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 -1\\) 階或第 \\(i - 2\\) 階邁向第 \\(i\\) 階。

    由此便可得出一個重要推論:爬到第 \\(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","level":2,"title":"14.1.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","level":2,"title":"14.1.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\\) 對應子問題的解。
    • 將最小子問題對應的狀態(第 \\(1\\) 階和第 \\(2\\) 階樓梯)稱為初始狀態。
    • 將遞推公式 \\(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\\) 輪決策組成的過程,對於每個物體都有不放入和放入兩種決策,因此該問題滿足決策樹模型。

    該問題的目標是求解“在限定背包容量下能放入物品的最大價值”,因此較大機率是一個動態規劃問題。

    第一步:思考每輪的決策,定義狀態,從而得到 \\(dp\\) 表

    對於每個物品來說,不放入背包,背包容量不變;放入背包,背包容量減小。由此可得狀態定義:當前物品編號 \\(i\\) 和背包容量 \\(c\\) ,記為 \\([i, c]\\) 。

    狀態 \\([i, c]\\) 對應的子問題為:前 \\(i\\) 個物品在容量為 \\(c\\) 的背包中的最大價值,記為 \\(dp[i, c]\\) 。

    待求解的是 \\(dp[n, cap]\\) ,因此需要一個尺寸為 \\((n+1) \\times (cap+1)\\) 的二維 \\(dp\\) 表。

    第二步:找出最優子結構,進而推導出狀態轉移方程

    當我們做出物品 \\(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\\) ,則只能選擇不放入背包。

    第三步:確定邊界條件和狀態轉移順序

    當無物品或背包容量為 \\(0\\) 時最大價值為 \\(0\\) ,即首列 \\(dp[i, 0]\\) 和首行 \\(dp[0, c]\\) 都等於 \\(0\\) 。

    當前狀態 \\([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","level":3,"title":"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","level":3,"title":"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","level":3,"title":"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 的第一維 \\(i\\) 直接刪除,並且把內迴圈更改為倒序走訪即可:

    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()\\) 改為求和運算子。

    編輯距離問題

    • 編輯距離(Levenshtein 距離)用於衡量兩個字串之間的相似度,其定義為從一個字串到另一個字串的最少編輯步數,編輯操作包括新增、刪除、替換。
    • 編輯距離問題的狀態定義為將 \\(s\\) 的前 \\(i\\) 個字元更改為 \\(t\\) 的前 \\(j\\) 個字元所需的最少編輯步數。當 \\(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":"

    對比兩道題目的程式碼,狀態轉移中有一處從 \\(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":"

    零錢兌換可以看作完全背包問題的一種特殊情況,兩者具有以下關聯與不同點。

    • 兩道題可以相互轉換,“物品”對應“硬幣”、“物品重量”對應“硬幣面值”、“背包容量”對應“目標金額”。
    • 最佳化目標相反,完全背包問題是要最大化物品價值,零錢兌換問題是要最小化硬幣數量。
    • 完全背包問題是求“不超過”背包容量下的解,零錢兌換是求“恰好”湊到目標金額的解。

    第一步:思考每輪的決策,定義狀態,從而得到 \\(dp\\) 表

    狀態 \\([i, a]\\) 對應的子問題為:前 \\(i\\) 種硬幣能夠湊出金額 \\(a\\) 的最少硬幣數量,記為 \\(dp[i, a]\\) 。

    二維 \\(dp\\) 表的尺寸為 \\((n+1) \\times (amt+1)\\) 。

    第二步:找出最優子結構,進而推導出狀態轉移方程

    本題與完全背包問題的狀態轉移方程存在以下兩點差異。

    • 本題要求最小值,因此需將運算子 \\(\\max()\\) 更改為 \\(\\min()\\) 。
    • 最佳化主體是硬幣數量而非商品價值,因此在選中硬幣時執行 \\(+1\\) 即可。
    \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

    第三步:確定邊界條件和狀態轉移順序

    當目標金額為 \\(0\\) 時,湊出它的最少硬幣數量為 \\(0\\) ,即首列所有 \\(dp[i, 0]\\) 都等於 \\(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]\\) 都初始化為 \\(1\\) 。當無硬幣時,無法湊出任何 \\(>0\\) 的目標金額,因此首行所有 \\(dp[0, a]\\) 都等於 \\(0\\) 。

    ","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 章   圖"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"本章內容","text":"
    • 9.1   圖
    • 9.2   圖基礎操作
    • 9.3   圖的走訪
    • 9.4   小結
    ","path":["第 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 所示。

    • 在無向圖中,邊表示兩頂點之間的“雙向”連線關係,例如微信或 QQ 中的“好友關係”。
    • 在有向圖中,邊具有方向性,即 \\(A \\rightarrow B\\) 和 \\(A \\leftarrow B\\) 兩個方向的邊是相互獨立的,例如微博或抖音上的“關注”與“被關注”關係。

    圖 9-2   有向圖與無向圖

    根據所有頂點是否連通,可分為連通圖(connected graph)和非連通圖(disconnected graph),如圖 9-3 所示。

    • 對於連通圖,從某個頂點出發,可以到達其餘任意頂點。
    • 對於非連通圖,從某個頂點出發,至少有一個頂點無法到達。

    圖 9-3   連通圖與非連通圖

    我們還可以為邊新增“權重”變數,從而得到如圖 9-4 所示的有權圖(weighted graph)。例如在《王者榮耀》等手遊中,系統會根據共同遊戲時間來計算玩家之間的“親密度”,這種親密度網路就可以用有權圖來表示。

    圖 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\\) 大小的矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 \\(1\\) 或 \\(0\\) 表示兩個頂點之間是否存在邊。

    如圖 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)\\) 時間。而由於是無向圖,因此需要同時更新兩個方向的邊。
    • 新增頂點:在鄰接矩陣的尾部新增一行一列,並全部填 \\(0\\) 即可,使用 \\(O(n)\\) 時間。
    • 刪除頂點:在鄰接矩陣中刪除一行一列。當刪除首行首列時達到最差情況,需要將 \\((n-1)^2\\) 個元素“向左上移動”,從而使用 \\(O(n^2)\\) 時間。
    • 初始化:傳入 \\(n\\) 個頂點,初始化長度為 \\(n\\) 的頂點串列 vertices ,使用 \\(O(n)\\) 時間;初始化 \\(n \\times n\\) 大小的鄰接矩陣 adjMat ,使用 \\(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\\) 的頂點,則需走訪整個鄰接表,將所有大於 \\(i\\) 的索引全部減 \\(1\\) ,效率很低。而如果每個頂點都是唯一的 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 的雜湊表,它可以在 \\(O(1)\\) 時間複雜度下進行 key 的增刪查改操作。根據 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 操作會返回新的的引用,必須讓原引用重新賦值為新slice的引用\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   圖的深度優先走訪步驟

    深度優先走訪的序列是否唯一?

    與廣度優先走訪類似,深度優先走訪序列的順序也不是唯一的。給定某頂點,先往哪個方向探索都可以,即鄰接頂點的順序可以任意打亂,都是深度優先走訪。

    以樹的走訪為例,“根 \\(\\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":"
    • 圖由頂點和邊組成,可以表示為一組頂點和一組邊構成的集合。
    • 相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)具有更高的自由度,因而更為複雜。
    • 有向圖的邊具有方向性,連通圖中的任意頂點均可達,有權圖的每條邊都包含權重變數。
    • 鄰接矩陣利用矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 \\(1\\) 或 \\(0\\) 表示兩個頂點之間有邊或無邊。鄰接矩陣在增刪查改操作上效率很高,但空間佔用較多。
    • 鄰接表使用多個鏈結串列來表示圖,第 \\(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 章   貪婪"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"本章內容","text":"
    • 15.1   貪婪演算法
    • 15.2   分數背包問題
    • 15.3   最大容量問題
    • 15.4   最大切分乘積問題
    • 15.5   小結
    ","path":["第 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(\\log n)\\) ,空間複雜度通常為 \\(O(\\log n)\\) 或 \\(O(n)\\) ,取決於程式語言的具體實現。

    除排序之外,在最差情況下,需要走訪整個物品串列,因此時間複雜度為 \\(O(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
    視覺化執行

    全螢幕觀看 >

    你可能會不由地發出感嘆:So clean !貪婪演算法僅用約十行程式碼就解決了零錢兌換問題。

    ","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":"

    貪婪演算法常常應用在滿足貪婪選擇性質和最優子結構的最佳化問題中,以下列舉了一些典型的貪婪演算法問題。

    • 硬幣找零問題:在某些硬幣組合下,貪婪演算法總是可以得到最優解。
    • 區間排程問題:假設你有一些任務,每個任務在一段時間內進行,你的目標是完成儘可能多的任務。如果每次都選擇結束時間最早的任務,那麼貪婪演算法就可以得到最優解。
    • 分數背包問題:給定一組物品和一個載重量,你的目標是選擇一組物品,使得總重量不超過載重量,且總價值最大。如果每次都選擇價效比最高(價值 / 重量)的物品,那麼貪婪演算法在一些情況下可以得到最優解。
    • 股票買賣問題:給定一組股票的歷史價格,你可以進行多次買賣,但如果你已經持有股票,那麼在賣出之前不能再買,目標是獲取最大利潤。
    • 霍夫曼編碼:霍夫曼編碼是一種用於無損資料壓縮的貪婪演算法。透過構建霍夫曼樹,每次選擇出現頻率最低的兩個節點合併,最後得到的霍夫曼樹的帶權路徑長度(編碼長度)最小。
    • Dijkstra 演算法:它是一種解決給定源頂點到其餘各頂點的最短路徑問題的貪婪演算法。
    ","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\\) 的整數都應該被切分。

    貪婪策略一:如果切分方案中包含 \\(\\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\\) 總是可以替換為兩個 \\(3\\) ,從而獲得更大的乘積。

    圖 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 所示,我們無須透過迴圈來切分整數,而可以利用向下整除運算得到 \\(3\\) 的個數 \\(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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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() 內部呼叫 C 語言庫的 pow() 函式,其執行浮點取冪,時間複雜度為 \\(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\\) :假設最優切分方案中存在 \\(\\geq 4\\) 的因子 \\(x\\) ,那麼一定可以將其繼續劃分為 \\(2(x-2)\\) ,從而獲得更大(或相等)的乘積。這與假設矛盾。
    2. 切分方案不包含 \\(1\\) :假設最優切分方案中存在一個因子 \\(1\\) ,那麼它一定可以合併入另外一個因子中,以獲得更大的乘積。這與假設矛盾。
    3. 切分方案最多包含兩個 \\(2\\) :假設最優切分方案中包含三個 \\(2\\) ,那麼一定可以替換為兩個 \\(3\\) ,乘積更大。這與假設矛盾。
    ","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 章   雜湊表"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"本章內容","text":"
    • 6.1   雜湊表
    • 6.2   雜湊衝突
    • 6.3   雜湊演算法
    • 6.4   小結
    ","path":["第 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 碼累積到雜湊值中。
    • 互斥或雜湊:將輸入資料的每個元素透過互斥或操作累積到一個雜湊值中。
    • 旋轉雜湊:將每個字元的 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    \"\"\"互斥或雜湊\"\"\"\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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### 互斥或雜湊 ###\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\\) 整除,那麼所有可以被 \\(3\\) 整除的 key 都會被對映到 \\(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\\) ,由於 keymodulus 之間不存在公約數,因此輸出的雜湊值的均勻性會明顯提升。

    \\[ \\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":"

    不難發現,以上介紹的簡單雜湊演算法都比較“脆弱”,遠遠沒有達到雜湊演算法的設計目標。例如,由於加法和互斥或滿足交換律,因此加法雜湊和互斥或雜湊無法區分內容相同但順序不同的字串,這可能會加劇雜湊衝突,並引起一些安全問題。

    在實際中,我們通常會用一些標準雜湊演算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它們可以將任意長度的輸入資料對映到恆定長度的雜湊值。

    近一個世紀以來,雜湊演算法處在不斷升級與最佳化的過程中。一部分研究人員努力提升雜湊演算法的效能,另一部分研究人員和駭客則致力於尋找雜湊演算法的安全性問題。表 6-2 展示了在實際應用中常見的雜湊演算法。

    • MD5 和 SHA-1 已多次被成功攻擊,因此它們被各類安全應用棄用。
    • SHA-2 系列中的 SHA-256 是最安全的雜湊演算法之一,仍未出現成功的攻擊案例,因此常用在各類安全應用與協議中。
    • 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 演算法\"\nhash_str = hash(str)\n# 字串“Hello 演算法”的雜湊值為 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 演算法\";\nsize_t hashStr = hash<string>()(str);\n// 字串“Hello 演算法”的雜湊值為 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 演算法\";\nint hashStr = str.hashCode();\n// 字串“Hello 演算法”的雜湊值為 -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 演算法\";\nint hashStr = str.GetHashCode();\n// 字串“Hello 演算法”的雜湊值為 -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 演算法\"\nlet hashStr = str.hashValue\n// 字串“Hello 演算法”的雜湊值為 -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 演算法\";\nint hashStr = str.hashCode;\n// 字串“Hello 演算法”的雜湊值為 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 演算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 字串“Hello 演算法”的雜湊值為 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 演算法\"\nval hashStr = str.hashCode()\n// 字串“Hello 演算法”的雜湊值為 -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 演算法\"\nhash_str = str.hash\n# 字串“Hello 演算法”的雜湊值為 -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
    視覺化執行

    全螢幕觀看 >

    在許多程式語言中,只有不可變物件才可作為雜湊表的 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 來標記這個桶。在該機制下,NoneTOMBSTONE 都代表空桶,都可以放置鍵值對。但不同的是,線性探查到 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 個鍵值對,超出容量則連線一個溢位桶;當溢位桶過多時,會執行一次特殊的等量擴容操作,以確保效能。
    ","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
    視覺化執行

    全螢幕觀看 >

    雜湊表有三種常用的走訪方式:走訪鍵值對、走訪鍵和走訪值。示例程式碼如下:

    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
    視覺化執行

    全螢幕觀看 >

    ","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 對應的鍵值對在陣列中的儲存位置。

    輸入一個 key ,雜湊函式的計算過程分為以下兩步。

    1. 透過某種雜湊演算法 hash() 計算得到雜湊值。
    2. 將雜湊值對桶數量(陣列長度)capacity 取模,從而獲取該 key 對應的桶(陣列索引)index
    index = hash(key) % capacity\n

    隨後,我們就可以利用 index 在雜湊表中訪問對應的桶,從而獲取 value

    設陣列長度 capacity = 100、雜湊演算法 hash(key) = key ,易得雜湊函式為 key % 100 。圖 6-2 以 key 學號和 value 姓名為例,展示了雜湊函式的工作原理。

    圖 6-2   雜湊函式工作原理

    以下程式碼實現了一個簡單雜湊表。其中,我們將 keyvalue 封裝成一個類別 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 ,雜湊表能夠在 \\(O(1)\\) 時間內查詢到 value ,效率非常高。
    • 常見的雜湊表操作包括查詢、新增鍵值對、刪除鍵值對和走訪雜湊表等。
    • 雜湊函式將 key 對映為陣列索引,從而訪問對應桶並獲取 value
    • 兩個不同的 key 可能在經過雜湊函式後得到相同的陣列索引,導致查詢結果出錯,這種現象被稱為雜湊衝突。
    • 雜湊表容量越大,雜湊衝突的機率就越低。因此可以透過擴容雜湊表來緩解雜湊衝突。與陣列擴容類似,雜湊表擴容操作的開銷很大。
    • 負載因子定義為雜湊表中元素數量除以桶數量,反映了雜湊衝突的嚴重程度,常用作觸發雜湊表擴容的條件。
    • 鏈式位址透過將單個元素轉化為鏈結串列,將所有衝突元素儲存在同一個鏈結串列中。然而,鏈結串列過長會降低查詢效率,可以透過進一步將鏈結串列轉換為紅黑樹來提高效率。
    • 開放定址透過多次探測來處理雜湊衝突。線性探查使用固定步長,缺點是不能刪除元素,且容易產生聚集。多次雜湊使用多個雜湊函式進行探測,相較線性探查更不易產生聚集,但多個雜湊函式增加了計算量。
    • 不同程式語言採取了不同的雜湊表實現。例如,Java 的 HashMap 使用鏈式位址,而 Python 的 Dict 採用開放定址。
    • 在雜湊表中,我們希望雜湊演算法具有確定性、高效率和均勻分佈的特點。在密碼學中,雜湊演算法還應該具備抗碰撞性和雪崩效應。
    • 雜湊演算法通常採用大質數作為模數,以最大化地保證雜湊值均勻分佈,減少雜湊衝突。
    • 常見的雜湊演算法包括 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 章   堆積"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"本章內容","text":"
    • 8.1   堆積
    • 8.2   建堆積操作
    • 8.3   Top-k 問題
    • 8.4   小結
    ","path":["第 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    # 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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        // 堆積化除葉節點以外的其他所有節點\n        h.siftDown(i)\n    }\n    return h\n}\n
    my_heap.swift
    /* 建構子,根據輸入串列建堆積 */\ninit(nums: [Int]) {\n    // 將串列元素原封不動新增進堆積\n    maxHeap = nums\n    // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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  // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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        // 堆積化除葉節點以外的其他所有節點\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        // 堆積化除葉節點以外的其他所有節點\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            // 當“越過根節點”或“節點無須修復”時,結束堆積化\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  # 堆積化除葉節點以外的其他所有節點\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)\\) 。但這個估算結果並不準確,因為我們沒有考慮到二元樹底層節點數量遠多於頂層節點的性質。

    接下來我們來進行更為準確的計算。為了降低計算難度,假設給定一個節點數量為 \\(n\\) 、高度為 \\(h\\) 的“完美二元樹”,該假設不會影響計算結果的正確性。

    圖 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() 訪問堆積頂元素(對於大 / 小頂堆積分別為最大 / 小值) \\(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# Python 的 heapq 模組預設實現小頂堆積\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// 初始化大頂堆積(使用 lambda 表示式修改 Comparator 即可)\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// 初始化大頂堆積(使用 lambda 表示式修改 Comparer 即可)\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// Swift 的 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// 初始化大頂堆積(使用 lambda 表示式修改 Comparator 即可)\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
    視覺化執行

    全螢幕觀看 >

    ","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 可能大於堆積中其他元素,堆積的成立條件可能已被破壞,因此需要修復從插入節點到根節點的路徑上的各個節點,這個操作被稱為堆積化(heapify)。

    考慮從入堆積節點開始,從底至頂執行堆積化。如圖 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        # 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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    // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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    # 當“越過根節點”或“節點無須修復”時,結束堆積化\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

    給定一個長度為 \\(n\\) 的無序陣列 nums ,請返回陣列中最大的 \\(k\\) 個元素。

    對於該問題,我們先介紹兩種思路比較直接的解法,再介紹效率更高的堆積解法。

    ","path":["第 8 章   堆積","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#831","level":2,"title":"8.3.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","level":2,"title":"8.3.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","level":2,"title":"8.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":"

    幾年前,我在力扣上分享了“劍指 Offer”系列題解,受到了許多讀者的鼓勵和支持。在與讀者交流期間,我最常被問的一個問題是“如何入門演算法”。逐漸地,我對這個問題產生了濃厚的興趣。

    兩眼一抹黑地刷題似乎是最受歡迎的方法,簡單、直接且有效。然而刷題就如同玩“掃雷”遊戲,自學能力強的人能夠順利將地雷逐個排掉,而基礎不足的人很可能被炸得滿頭是包,並在挫折中步步退縮。通讀教材也是一種常見做法,但對於面向求職的人來說,畢業論文、投遞簡歷、準備筆試和面試已經消耗了大部分精力,啃厚重的書往往變成了一項艱鉅的挑戰。

    如果你也面臨類似的困擾,那麼很幸運這本書“找”到了你。本書是我對這個問題給出的答案,即使不是最優解,也至少是一次積極的嘗試。本書雖然不足以讓你直接拿到 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 章   初識演算法"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"本章內容","text":"
    • 1.1   演算法無處不在
    • 1.2   演算法是什麼
    • 1.3   小結
    ","path":["第 1 章   初識演算法"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   演算法無處不在","text":"

    當我們聽到“演算法”這個詞時,很自然地會想到數學。然而實際上,許多演算法並不涉及複雜數學,而是更多地依賴基本邏輯,這些邏輯在我們的日常生活中處處可見。

    在正式探討演算法之前,有一個有趣的事實值得分享:你已經在不知不覺中學會了許多演算法,並習慣將它們應用到日常生活中了。下面我將舉幾個具體的例子來證實這一點。

    例一:查字典。在字典裡,每個漢字都對應一個拼音,而字典是按照拼音字母順序排列的。假設我們需要查詢一個拼音首字母為 \\(r\\) 的字,通常會按照圖 1-1 所示的方式實現。

    1. 翻開字典約一半的頁數,檢視該頁的首字母是什麼,假設首字母為 \\(m\\) 。
    2. 由於在拼音字母表中 \\(r\\) 位於 \\(m\\) 之後,所以排除字典前半部分,查詢範圍縮小到後半部分。
    3. 不斷重複步驟 1. 和步驟 2. ,直至找到拼音首字母為 \\(r\\) 的頁碼為止。
    <1><2><3><4><5>

    圖 1-1   查字典步驟

    查字典這個小學生必備技能,實際上就是著名的“二分搜尋”演算法。從資料結構的角度,我們可以把字典視為一個已排序的“陣列”;從演算法的角度,我們可以將上述查字典的一系列操作看作“二分搜尋”。

    例二:整理撲克。我們在打牌時,每局都需要整理手中的撲克牌,使其從小到大排列,實現流程如圖 1-2 所示。

    1. 將撲克牌劃分為“有序”和“無序”兩部分,並假設初始狀態下最左 1 張撲克牌已經有序。
    2. 在無序部分抽出一張撲克牌,插入至有序部分的正確位置;完成後最左 2 張撲克已經有序。
    3. 不斷迴圈步驟 2. ,每一輪將一張撲克牌從無序部分插入至有序部分,直至所有撲克牌都有序。

    圖 1-2   撲克排序步驟

    上述整理撲克牌的方法本質上是“插入排序”演算法,它在處理小型資料集時非常高效。許多程式語言的排序庫函式中都有插入排序的身影。

    例三:貨幣找零。假設我們在超市購買了 \\(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 章   前言"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"本章內容","text":"
    • 0.1   關於本書
    • 0.2   如何使用本書
    • 0.3   小結
    ","path":["第 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 開發。感謝他的貢獻,為讀者提供了更靈活的閱讀方式。

    在本書的創作過程中,我得到了許多人的幫助。

    • 感謝我在公司的導師李汐博士,在一次暢談中你鼓勵我“快行動起來”,堅定了我寫這本書的決心;
    • 感謝我的女朋友泡泡作為本書的首位讀者,從演算法小白的角度提出許多寶貴建議,使得本書更適合新手閱讀;
    • 感謝騰寶、琦寶、飛寶為本書起了一個富有創意的名字,喚起大家寫下第一行程式碼“Hello World!”的美好回憶;
    • 感謝校銓在智慧財產權方面提供的專業幫助,這對本開源書的完善起到了重要作用;
    • 感謝蘇潼為本書設計了精美的封面和 logo ,並在我的強迫症的驅使下多次耐心修改;
    • 感謝 @squidfunk 提供的排版建議,以及他開發的開源文件主題 Material-for-MkDocs 。

    在寫作過程中,我閱讀了許多關於資料結構與演算法的教材和文章。這些作品為本書提供了優秀的範本,確保了本書內容的準確性與品質。在此感謝所有老師和前輩的傑出貢獻!

    本書倡導手腦並用的學習方式,在這一點上我深受《動手學深度學習》的啟發。在此向各位讀者強烈推薦這本優秀的著作。

    衷心感謝我的父母,正是你們一直以來的支持與鼓勵,讓我有機會做這件富有趣味的事。

    ","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   執行程式碼示例

    執行程式碼的前置工作主要分為三步。

    第一步:安裝本地程式設計環境。請參照附錄所示的教程進行安裝,如果已安裝,則可跳過此步驟。

    第二步:克隆或下載程式碼倉庫。前往 GitHub 倉庫。如果已經安裝 Git ,可以透過以下命令克隆本倉庫:

    git clone https://github.com/krahets/hello-algo.git\n

    當然,你也可以在圖 0-4 所示的位置,點選“Download ZIP”按鈕直接下載程式碼壓縮包,然後在本地解壓即可。

    圖 0-4   克隆倉庫與下載程式碼

    第三步:執行源程式碼。如圖 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. 階段一:演算法入門。我們需要熟悉各種資料結構的特點和用法,學習不同演算法的原理、流程、用途和效率等方面的內容。
    2. 階段二:刷演算法題。建議從熱門題目開刷,先積累至少 100 道題目,熟悉主流的演算法問題。初次刷題時,“知識遺忘”可能是一個挑戰,但請放心,這是很正常的。我們可以按照“艾賓浩斯遺忘曲線”來複習題目,通常在進行 3~5 輪的重複後,就能將其牢記在心。推薦的題單和刷題計劃請見此 GitHub 倉庫。
    3. 階段三:搭建知識體系。在學習方面,我們可以閱讀演算法專欄文章、解題框架和演算法教材,以不斷豐富知識體系。在刷題方面,可以嘗試採用進階刷題策略,如按專題分類、一題多解、一解多題等,相關的刷題心得可以在各個社群找到。

    如圖 0-8 所示,本書內容主要涵蓋“階段一”,旨在幫助你更高效地展開階段二和階段三的學習。

    圖 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, et al. Introduction to Algorithms (3rd Edition).

    [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

    [3] Robert Sedgewick, et al. Algorithms (4th Edition).

    [4] 嚴蔚敏. 資料結構(C 語言版).

    [5] 鄧俊輝. 資料結構(C++ 語言版,第三版).

    [6] 馬克 艾倫 維斯著,陳越譯. 資料結構與演算法分析:Java語言描述(第三版).

    [7] 程傑. 大話資料結構.

    [8] 王爭. 資料結構與演算法之美.

    [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

    [10] Aston Zhang, et al. Dive into Deep Learning.

    ","path":["參考文獻"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章   搜尋","text":"

    Abstract

    搜尋是一場未知的冒險,我們或許需要走遍神秘空間的每個角落,又或許可以快速鎖定目標。

    在這場尋覓之旅中,每一次探索都可能得到一個未曾料想的答案。

    ","path":["第 10 章   搜尋"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"本章內容","text":"
    • 10.1   二分搜尋
    • 10.2   二分搜尋插入點
    • 10.3   二分搜尋邊界
    • 10.4   雜湊最佳化策略
    • 10.5   重識搜尋演算法
    • 10.6   小結
    ","path":["第 10 章   搜尋"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   二分搜尋","text":"

    二分搜尋(binary search)是一種基於分治策略的高效搜尋演算法。它利用資料的有序性,每輪縮小一半搜尋範圍,直至找到目標元素或搜尋區間為空為止。

    Question

    給定一個長度為 \\(n\\) 的陣列 nums ,元素按從小到大的順序排列且不重複。請查詢並返回元素 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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 次除法、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

    給定一個長度為 \\(n\\) 的有序陣列 nums ,其中可能包含重複元素。請返回陣列中最左一個元素 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 的元素。

    因此,如圖 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

    給定一個長度為 \\(n\\) 的有序陣列 nums 和一個元素 target ,陣列不存在重複元素。現將 target 插入陣列 nums 中,並保持其有序性。若陣列中已存在元素 target ,則插入到其左方。請返回插入後 target 在陣列中的索引。示例如圖 10-4 所示。

    圖 10-4   二分搜尋插入點示例資料

    如果想複用上一節的二分搜尋程式碼,則需要回答以下兩個問題。

    問題一:當陣列中包含 target 時,插入點的索引是否是該元素的索引?

    題目要求將 target 插入到相等元素的左邊,這意味著新插入的 target 替換了原來 target 的位置。也就是說,當陣列包含 target 時,插入點的索引就是該 target 的索引。

    問題二:當陣列中不存在 target 時,插入點是哪個元素的索引?

    進一步思考二分搜尋過程:當 nums[m] < target 時 \\(i\\) 移動,這意味著指標 \\(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

    題目要求將目標元素插入到最左邊,所以我們需要查詢陣列中最左一個 target 的索引。初步考慮透過圖 10-5 所示的步驟實現。

    1. 執行二分搜尋,得到任意一個 target 的索引,記為 \\(k\\) 。
    2. 從索引 \\(k\\) 開始,向左進行線性走訪,當找到最左邊的 target 時返回。

    圖 10-5   線性查詢重複元素的插入點

    此方法雖然可用,但其包含線性查詢,因此時間複雜度為 \\(O(n)\\) 。當陣列中存在很多重複的 target 時,該方法效率很低。

    現考慮拓展二分搜尋程式碼。如圖 10-6 所示,整體流程保持不變,每輪先計算中點索引 \\(m\\) ,再判斷 targetnums[m] 的大小關係,分為以下幾種情況。

    • nums[m] < targetnums[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] > targetnums[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    \"\"\"方法一:暴力列舉\"\"\"\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一: 暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    ### 方法一:暴力列舉 ###\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    \"\"\"方法二:輔助雜湊表\"\"\"\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二: 輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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/* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    ### 方法二:輔助雜湊表 ###\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 章   排序"],"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 章   排序"],"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_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":"

    桶排序適用於處理體量很大的資料。例如,輸入資料包含 100 萬個元素,由於空間限制,系統記憶體無法一次性載入所有資料。此時,可以將資料分成 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 + 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":"

    先來看一個簡單的例子。給定一個長度為 \\(n\\) 的陣列 nums ,其中的元素都是“非負整數”,計數排序的整體流程如圖 11-16 所示。

    1. 走訪陣列,找出其中的最大數字,記為 \\(m\\) ,然後建立一個長度為 \\(m + 1\\) 的輔助陣列 counter
    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    # 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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  // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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  # 使用結果陣列 res 覆蓋原陣列 nums\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)\\)、非原地排序:藉助了長度分別為 \\(n\\) 和 \\(m\\) 的陣列 rescounter
    • 穩定排序:由於向 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. 從堆積頂元素開始,從頂到底執行堆積化操作(sift down)。完成堆積化後,堆積的性質得到修復。
    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    # 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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  // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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  # 建堆積操作:堆積化除葉節點以外的其他所有節點\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. 初始狀態下,陣列的第 1 個元素已完成排序。
    2. 選取陣列的第 2 個元素作為 base ,將其插入到正確位置後,陣列的前 2 個元素已排序。
    3. 選取第 3 個元素作為 base ,將其插入到正確位置後,陣列的前 3 個元素已排序。
    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. 選取陣列最左端元素作為基準數,初始化兩個指標 ij 分別指向陣列的兩端。
    2. 設定一個迴圈,在每輪中使用 ij)分別尋找第一個比基準數大(小)的元素,然後交換這兩個元素。
    3. 迴圈執行步驟 2. ,直到 ij 相遇時停止,最後將基準數交換至兩個子陣列的分界線。
    <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 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 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   基數排序演算法流程

    下面剖析程式碼實現。對於一個 \\(d\\) 進位制的數字 \\(x\\) ,要獲取其第 \\(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    \"\"\"獲取元素 num 的第 k 位,其中 exp = 10^(k-1)\"\"\"\n    # 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"計數排序(根據 nums 第 k 位排序)\"\"\"\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)  # 獲取 nums[i] 第 k 位,記為 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  # 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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) // 獲取 nums[i] 第 k 位,記為 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 // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    (num / exp) % 10\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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) // 獲取 nums[i] 第 k 位,記為 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 // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 _num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n  return (_num ~/ exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return ((num / exp) % 10) as usize;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 初始化為 0 以支持後續記憶體釋放\n    // 統計 0~9 各數字的出現次數\n    for (int i = 0; i < size; i++) {\n        // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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) // 獲取 nums[i] 第 k 位,記為 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 // 獲取 d 在陣列中的索引 j\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
    ### 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) ###\ndef digit(num, exp)\n  # 轉入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n  (num / exp) % 10\nend\n\n### 計數排序(根據 nums 第 k 位排序)###\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) # 獲取 nums[i] 第 k 位,記為 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 # 獲取 d 在陣列中的索引 j\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)\\)、非原地排序:與計數排序相同,基數排序需要藉助長度為 \\(n\\) 和 \\(d\\) 的陣列 rescounter
    • 穩定排序:當計數排序穩定時,基數排序也穩定;當計數排序不穩定時,基數排序無法保證得到正確的排序結果。
    ","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\\) 處的元素交換。完成後,陣列前 1 個元素已排序。
    3. 選取區間 \\([1, n-1]\\) 中的最小元素,將其與索引 \\(1\\) 處的元素交換。完成後,陣列前 2 個元素已排序。
    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":"

    執行效率:我們期望排序演算法的時間複雜度儘量低,且總體操作數量較少(時間複雜度中的常數項變小)。對於大資料量的情況,執行效率顯得尤為重要。

    就地性:顧名思義,原地排序透過在原陣列上直接操作實現排序,無須藉助額外的輔助陣列,從而節省記憶體。通常情況下,原地排序的資料搬運操作較少,執行速度也更快。

    穩定性:穩定排序在完成排序後,相等元素在陣列中的相對順序不發生改變。

    穩定排序是多級排序場景的必要條件。假設我們有一個儲存學生資訊的表格,第 1 列和第 2 列分別是姓名和年齡。在這種情況下,非穩定排序可能導致輸入資料的有序性喪失:

    # 輸入資料是按照姓名排序好的\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-q-a","level":3,"title":"2.   Q & A","text":"

    Q:排序演算法穩定性在什麼情況下是必需的?

    在現實中,我們有可能基於物件的某個屬性進行排序。例如,學生有姓名和身高兩個屬性,我們希望實現一個多級排序:先按照姓名進行排序,得到 (A, 180) (B, 185) (C, 170) (D, 170) ;再對身高進行排序。由於排序演算法不穩定,因此可能得到 (D, 170) (C, 170) (A, 180) (B, 185)

    可以發現,學生 D 和 C 的位置發生了交換,姓名的有序性被破壞了,而這是我們不希望看到的。

    Q:哨兵劃分中“從右往左查詢”與“從左往右查詢”的順序可以交換嗎?

    不行,當我們以最左端元素為基準數時,必須先“從右往左查詢”再“從左往右查詢”。這個結論有些反直覺,我們來剖析一下原因。

    哨兵劃分 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] 為基準數,那麼正好反過來,必須先“從左往右查詢”。

    Q:關於快速排序的遞迴深度最佳化,為什麼選短的陣列能保證遞迴深度不超過 \\(\\log n\\) ?

    遞迴深度就是當前未返回的遞迴方法的數量。每輪哨兵劃分我們將原陣列劃分為兩個子陣列。在遞迴深度最佳化後,向下遞迴的子陣列長度最大為原陣列長度的一半。假設最差情況,一直為一半長度,那麼最終的遞迴深度就是 \\(\\log n\\) 。

    回顧原始的快速排序,我們有可能會連續地遞迴長度較大的陣列,最差情況下為 \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) ,遞迴深度為 \\(n\\) 。遞迴深度最佳化可以避免這種情況出現。

    Q:當陣列中所有元素都相等時,快速排序的時間複雜度是 \\(O(n^2)\\) 嗎?該如何處理這種退化情況?

    是的。對於這種情況,可以考慮透過哨兵劃分將陣列劃分為三個部分:小於、等於、大於基準數。僅向下遞迴小於和大於的兩部分。在該方法下,輸入元素全部相等的陣列,僅一輪哨兵劃分即可完成排序。

    Q:桶排序的最差時間複雜度為什麼是 \\(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 章   堆疊與佇列"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"本章內容","text":"
    • 5.1   堆疊
    • 5.2   佇列
    • 5.3   雙向佇列
    • 5.4   小結
    ","path":["第 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
    視覺化執行

    全螢幕觀看 >

    ","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":"

    回顧上一節內容,我們使用普通單向鏈結串列來實現佇列,因為它可以方便地刪除頭節點(對應出列操作)和在尾節點後新增新節點(對應入列操作)。

    對於雙向佇列而言,頭部和尾部都可以執行入列和出列操作。換句話說,雙向佇列需要實現另一個對稱方向的操作。為此,我們採用“雙向鏈結串列”作為雙向佇列的底層資料結構。

    如圖 5-8 所示,我們將雙向鏈結串列的頭節點和尾節點視為雙向佇列的佇列首和佇列尾,同時實現在兩端新增和刪除節點的功能。

    <1><2><3><4><5>

    圖 5-8   基於鏈結串列實現雙向佇列的入列出列操作

    實現程式碼如下所示:

    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   基於陣列實現雙向佇列的入列出列操作

    在佇列的實現基礎上,僅需增加“佇列首入列”和“佇列尾出列”的方法:

    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;        // 佇列首指標,指向佇列首元素\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; // 佇列首指標,指向佇列首元素\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;   // 佇列首指標,指向佇列首元素\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   // 佇列首指標,指向佇列首元素\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 // 佇列首指標,指向佇列首元素\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; // 佇列首指標,指向佇列首元素\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; // 佇列首指標,指向佇列首元素\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; // 佇列首指標,指向佇列首元素\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,    // 佇列首指標,指向佇列首元素\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;       // 佇列首指標,指向佇列首元素\n    int queSize;     // 尾指標,指向佇列尾 + 1\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 // 佇列首指標,指向佇列首元素\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":"

    雙向佇列兼具堆疊與佇列的邏輯,因此它可以實現這兩者的所有應用場景,同時提供更高的自由度。

    我們知道,軟體的“撤銷”功能通常使用堆疊來實現:系統將每次更改操作 push 到堆疊中,然後透過 pop 實現撤銷。然而,考慮到系統資源的限制,軟體通常會限制撤銷的步數(例如僅允許儲存 \\(50\\) 步)。當堆疊的長度超過 \\(50\\) 時,軟體需要在堆疊底(佇列首)執行刪除操作。但堆疊無法實現該功能,此時就需要使用雙向佇列來替代堆疊。請注意,“撤銷”的核心邏輯仍然遵循堆疊的先入後出原則,只是雙向佇列能夠更加靈活地實現一些額外邏輯。

    ","path":["第 5 章   堆疊與佇列","5.3   雙向佇列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   佇列","text":"

    佇列(queue)是一種遵循先入先出規則的線性資料結構。顧名思義,佇列模擬了排隊現象,即新來的人不斷加入佇列尾部,而位於佇列頭部的人逐個離開。

    如圖 5-4 所示,我們將佇列頭部稱為“佇列首”,尾部稱為“佇列尾”,將把元素加入列尾的操作稱為“入列”,刪除佇列首元素的操作稱為“出列”。

    圖 5-4   佇列的先入先出規則

    ","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 中,佇列類別 Qeque 是雙向佇列,也可作為佇列使用\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 和走訪方法,可以把 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
    視覺化執行

    全螢幕觀看 >

    ","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   基於鏈結串列實現佇列的入列出列操作

    以下是用鏈結串列實現佇列的程式碼:

    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        # 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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    // 如果佇列為空,則令頭、尾節點都指向該節點\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            // 如果佇列為空,則令頭、尾節點都指向該節點\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    // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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    # 如果佇列為空,則令頭,尾節點都指向該節點\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)\\) ,這會導致出列操作效率較低。然而,我們可以採用以下巧妙方法來避免這個問題。

    我們可以使用一個變數 front 指向佇列首元素的索引,並維護一個變數 size 用於記錄佇列長度。定義 rear = front + size ,這個公式計算出的 rear 指向佇列尾元素之後的下一個位置。

    基於此設計,陣列中包含元素的有效區間為 [front, rear - 1],各種操作的實現方法如圖 5-6 所示。

    • 入列操作:將輸入元素賦值給 rear 索引處,並將 size 增加 1 。
    • 出列操作:只需將 front 增加 1 ,並將 size 減少 1 。

    可以看到,入列和出列操作都只需進行一次操作,時間複雜度均為 \\(O(1)\\) 。

    <1><2><3>

    圖 5-6   基於陣列實現佇列的入列出列操作

    你可能會發現一個問題:在不斷進行入列和出列的過程中,frontrear 都在向右移動,當它們到達陣列尾部時就無法繼續移動了。為了解決此問題,我們可以將陣列視為首尾相接的“環形陣列”。

    對於環形陣列,我們需要讓 frontrear 在越過陣列尾部時,直接回到陣列頭部繼續走訪。這種週期性規律可以透過“取餘操作”來實現,程式碼如下所示:

    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  # 佇列首指標,指向佇列首元素\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        # 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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;       // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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; // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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;   // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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   // 佇列首指標,指向佇列首元素\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    // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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 // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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; // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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; // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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; // 佇列首指標,指向佇列首元素\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    // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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,        // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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;       // 佇列首指標,指向佇列首元素\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    // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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 // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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 # 佇列首指標,指向佇列首元素\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    # 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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 所示,我們把堆積疊元素的頂部稱為“堆疊頂”,底部稱為“堆疊底”。將把元素新增到堆疊頂的操作叫作“入堆疊”,刪除堆疊頂元素的操作叫作“出堆疊”。

    圖 5-1   堆疊的先入後出規則

    ","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
    視覺化執行

    全螢幕觀看 >

    ","path":["第 5 章   堆疊與佇列","5.1   堆疊"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   堆疊的實現","text":"

    為了深入瞭解堆疊的執行機制,我們來嘗試自己實現一個堆疊類別。

    堆疊遵循先入後出的原則,因此我們只能在堆疊頂新增或刪除元素。然而,陣列和鏈結串列都可以在任意位置新增和刪除元素,因此堆疊可以視為一種受限制的陣列或鏈結串列。換句話說,我們可以“遮蔽”陣列或鏈結串列的部分無關操作,使其對外表現的邏輯符合堆疊的特性。

    ","path":["第 5 章   堆疊與佇列","5.1   堆疊"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   基於鏈結串列的實現","text":"

    使用鏈結串列實現堆疊時,我們可以將鏈結串列的頭節點視為堆疊頂,尾節點視為堆疊底。

    如圖 5-2 所示,對於入堆疊操作,我們只需將元素插入鏈結串列頭部,這種節點插入方法被稱為“頭插法”。而對於出堆疊操作,只需將頭節點從鏈結串列中刪除即可。

    <1><2><3>

    圖 5-2   基於鏈結串列實現堆疊的入堆疊出堆疊操作

    以下是基於鏈結串列實現堆疊的示例程式碼:

    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 所示,入堆疊與出堆疊操作分別對應在陣列尾部新增元素與刪除元素,時間複雜度都為 \\(O(1)\\) 。

    <1><2><3>

    圖 5-3   基於陣列實現堆疊的入堆疊出堆疊操作

    由於入堆疊的元素可能會源源不斷地增加,因此我們可以使用動態陣列,這樣就無須自行處理陣列擴容問題。以下為示例程式碼:

    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":"

    支持操作

    兩種實現都支持堆疊定義中的各項操作。陣列實現額外支持隨機訪問,但這已超出了堆疊的定義範疇,因此一般不會用到。

    時間效率

    在基於陣列的實現中,入堆疊和出堆疊操作都在預先分配好的連續記憶體中進行,具有很好的快取本地性,因此效率較高。然而,如果入堆疊時超出陣列容量,會觸發擴容機制,導致該次入堆疊操作的時間複雜度變為 \\(O(n)\\) 。

    在基於鏈結串列的實現中,鏈結串列的擴容非常靈活,不存在上述陣列擴容時效率降低的問題。但是,入堆疊操作需要初始化節點物件並修改指標,因此效率相對較低。不過,如果入堆疊元素本身就是節點物件,那麼可以省去初始化步驟,從而提高效率。

    綜上所述,當入堆疊與出堆疊操作的元素是基本資料型別時,例如 intdouble ,我們可以得出以下結論。

    • 基於陣列實現的堆疊在觸發擴容時效率會降低,但由於擴容是低頻操作,因此平均效率更高。
    • 基於鏈結串列實現的堆疊可以提供更加穩定的效率表現。

    空間效率

    在初始化串列時,系統會為串列分配“初始容量”,該容量可能超出實際需求;並且,擴容機制通常是按照特定倍率(例如 2 倍)進行擴容的,擴容後的容量也可能超出實際需求。因此,基於陣列實現的堆疊可能造成一定的空間浪費。

    然而,由於鏈結串列節點需要額外儲存指標,因此鏈結串列節點佔用的空間相對較大。

    綜上,我們不能簡單地確定哪種實現更加節省記憶體,需要針對具體情況進行分析。

    ","path":["第 5 章   堆疊與佇列","5.1   堆疊"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   堆疊的典型應用","text":"
    • 瀏覽器中的後退與前進、軟體中的撤銷與反撤銷。每當我們開啟新的網頁,瀏覽器就會對上一個網頁執行入堆疊,這樣我們就可以通過後退操作回到上一個網頁。後退操作實際上是在執行出堆疊。如果要同時支持後退和前進,那麼需要兩個堆疊來配合實現。
    • 程式記憶體管理。每次呼叫函式時,系統都會在堆疊頂新增一個堆疊幀,用於記錄函式的上下文資訊。在遞迴函式中,向下遞推階段會不斷執行入堆疊操作,而向上回溯階段則會不斷執行出堆疊操作。
    ","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":"
    • 堆疊是一種遵循先入後出原則的資料結構,可透過陣列或鏈結串列來實現。
    • 在時間效率方面,堆疊的陣列實現具有較高的平均效率,但在擴容過程中,單次入堆疊操作的時間複雜度會劣化至 \\(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:在出堆疊後,是否需要釋放出堆疊節點的記憶體?

    如果後續仍需要使用彈出節點,則不需要釋放記憶體。若之後不需要用到,JavaPython 等語言擁有自動垃圾回收機制,因此不需要手動釋放記憶體;在 CC++ 中需要手動釋放記憶體。

    Q:雙向佇列像是兩個堆疊拼接在了一起,它的用途是什麼?

    雙向佇列就像是堆疊和佇列的組合或兩個堆疊拼在了一起。它表現的是堆疊 + 佇列的邏輯,因此可以實現堆疊與佇列的所有應用,並且更加靈活。

    Q:撤銷(undo)和反撤銷(redo)具體是如何實現的?

    使用兩個堆疊,堆疊 A 用於撤銷,堆疊 B 用於反撤銷。

    1. 每當使用者執行一個操作,將這個操作壓入堆疊 A ,並清空堆疊 B
    2. 當用戶執行“撤銷”時,從堆疊 A 中彈出最近的操作,並將其壓入堆疊 B
    3. 當用戶執行“反撤銷”時,從堆疊 B 中彈出最近的操作,並將其壓入堆疊 A
    ","path":["第 5 章   堆疊與佇列","5.4   小結"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章   樹","text":"

    Abstract

    參天大樹充滿生命力,根深葉茂,分枝扶疏。

    它為我們展現了資料分治的生動形態。

    ","path":["第 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 章   樹"],"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 ,因此我們無法僅憑該序列來推測 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// 使用 int 的包裝類別 Integer ,就可以使用 null 來標記空位\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
    /* 二元樹的陣列表示 */\n// 使用 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// 使用 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// 使用 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 年 G. M. Adelson-Velsky 和 E. M. 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 樹既是二元搜尋樹,也是平衡二元樹,同時滿足這兩類二元樹的所有性質,因此是一種平衡二元搜尋樹(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    # 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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  // 以 child 為原點,將 node 向右旋轉\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            // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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  # 以 child 為原點,將 node 向右旋轉\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_1","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    # 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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  // 以 child 為原點,將 node 向左旋轉\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            // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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  # 以 child 為原點,將 node 向左旋轉\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":"

    對於圖 7-30 中的失衡節點 3 ,僅使用左旋或右旋都無法使子樹恢復平衡。此時需要先對 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-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_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_2","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_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.valnum 之間的大小關係。

    • 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 or 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        # 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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    // 用 tmp 覆蓋 cur\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 or 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            // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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    # 用 tmp 覆蓋 cur\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
    視覺化執行

    全螢幕觀看 >

    ","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# 在 n1 -> n2 中間插入節點 P\nn1.left = p\np.left = n2\n# 刪除節點 P\nn1.left = n2\n
    binary_tree.cpp
    /* 插入與刪除節點 */\nTreeNode* P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1->left = P;\nP->left = n2;\n// 刪除節點 P\nn1->left = n2;\n// 釋放記憶體\ndelete P;\n
    binary_tree.java
    TreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.cs
    /* 插入與刪除節點 */\nTreeNode P = new(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.go
    /* 插入與刪除節點 */\n// 在 n1 -> n2 中間插入節點 P\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// 刪除節點 P\nn1.Left = n2\n
    binary_tree.swift
    let P = TreeNode(x: 0)\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P\nP.left = n2\n// 刪除節點 P\nn1.left = n2\n
    binary_tree.js
    /* 插入與刪除節點 */\nlet P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.ts
    /* 插入與刪除節點 */\nconst P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.dart
    /* 插入與刪除節點 */\nTreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.rs
    let p = TreeNode::new(0);\n// 在 n1 -> n2 中間插入節點 P\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// 在 n1 -> n2 中間插入節點 P\nn1->left = P;\nP->left = n2;\n// 刪除節點 P\nn1->left = n2;\n// 釋放記憶體\nfree(P);\n
    binary_tree.kt
    val P = TreeNode(0)\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P\nP.left = n2\n// 刪除節點 P\nn1.left = n2\n
    binary_tree.rb
    # 插入與刪除節點\n_p = TreeNode.new(0)\n# 在 n1 -> n2 中間插入節點 _p\nn1.left = _p\n_p.left = n2\n# 刪除節點\nn1.left = n2\n
    視覺化執行

    全螢幕觀看 >

    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:右旋操作是處理失衡節點 nodechildgrand_child 之間的關係,那 node 的父節點和 node 原來的連線不需要維護嗎?右旋操作後豈不是斷掉了?

    我們需要從遞迴的視角來看這個問題。右旋操作 right_rotate(root) 傳入的是子樹的根節點,最終 return child 返回旋轉之後的子樹的根節點。子樹的根節點和其父節點的連線是在該函式返回後完成的,不屬於右旋操作的維護範圍。

    Q:在 C++ 中,函式被劃分到 privatepublic 中,這方面有什麼考量嗎?為什麼要將 height() 函式和 updateHeight() 函式分別放在 publicprivate 中呢?

    主要看方法的使用範圍,如果方法只在類別內部使用,那麼就設計為 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 章   附錄"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"本章內容","text":"
    • 16.1   程式設計環境安裝
    • 16.2   一起參與創作
    • 16.3   術語表
    ","path":["第 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. 點選“編輯圖示”,如果遇到“需要 Fork 此倉庫”的提示,請同意該操作。
    2. 修改 Markdown 源檔案內容,檢查內容的正確性,並儘量保持排版格式的統一。
    3. 在頁面底部填寫修改說明,然後點選“Propose file change”按鈕。頁面跳轉後,點選“Create 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. 將本地所做更改 Commit ,然後 Push 至遠端倉庫。
    5. 重新整理倉庫網頁,點選“Create 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":"

    推薦使用開源、輕量的 VS Code 作為本地整合開發環境(IDE)。訪問 VS Code 官網,根據作業系統選擇相應版本的 VS Code 進行下載和安裝。

    圖 16-1   從官網下載 VS Code

    VS Code 擁有強大的擴展包生態系統,支持大多數程式語言的執行和除錯。以 Python 為例,安裝“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 大 \\(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 AVL 樹 red-black tree 紅黑樹 level-order traversal 層序走訪 breadth-first traversal 廣度優先走訪 depth-first traversal 深度優先走訪 binary search tree 二元搜尋樹 balanced binary search tree 平衡二元搜尋樹 balance factor 平衡因子 heap 堆積 max heap 大頂堆積 min heap 小頂堆積 priority queue 優先佇列 heapify 堆積化 top-\\(k\\) problem Top-\\(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 章   陣列與鏈結串列"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"本章內容","text":"
    • 4.1   陣列
    • 4.2   鏈結串列
    • 4.3   串列
    • 4.4   記憶體與快取 *
    • 4.5   小結
    ","path":["第 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":"

    我們可以根據需求選用陣列的兩種初始化方式:無初始值、給定初始值。在未指定初始值的情況下,大多數程式語言會將陣列元素初始化為 \\(0\\) :

    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
    視覺化執行

    全螢幕觀看 >

    ","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    \"\"\"在陣列的索引 index 處插入元素 num\"\"\"\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 _num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    /* 在陣列的索引 index 處插入元素 num */\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
    ### 在陣列的索引 index 處插入元素 num ###\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// 請注意,JavaScript 的 Array 是動態陣列,可以直接擴展\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// 請注意,TypeScript 的 Array 是動態陣列,可以直接擴展\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# 請注意,Ruby 的 Array 是動態陣列,可以直接擴展\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 中分別被記為 nullnullptrNone
    • 在 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
    視覺化執行

    全螢幕觀看 >

    陣列整體是一個變數,比如陣列 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 所示,假設我們想在相鄰的兩個節點 n0n1 之間插入一個新節點 P ,則只需改變兩個節點引用(指標)即可,時間複雜度為 \\(O(1)\\) 。

    相比之下,在陣列中插入元素的時間複雜度為 \\(O(n)\\) ,在大資料量下的效率較低。

    圖 4-6   鏈結串列插入節點示例

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
    def insert(n0: ListNode, P: ListNode):\n    \"\"\"在鏈結串列的節點 n0 之後插入節點 P\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
    linked_list.cpp
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.java
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.cs
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.go
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
    linked_list.swift
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
    linked_list.js
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
    linked_list.ts
    /* 在鏈結串列的節點 n0 之後插入節點 P */\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
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
    linked_list.rs
    /* 在鏈結串列的節點 n0 之後插入節點 P */\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
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
    linked_list.kt
    /* 在鏈結串列的節點 n0 之後插入節點 P */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
    linked_list.rb
    ### 在鏈結串列的節點 n0 之後插入節點 _p ###\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-vs","level":2,"title":"4.2.2   陣列 vs. 鏈結串列","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":"

    單向鏈結串列通常用於實現堆疊、佇列、雜湊表和圖等資料結構。

    • 堆疊與佇列:當插入和刪除操作都在鏈結串列的一端進行時,它表現的特性為先進後出,對應堆疊;當插入操作在鏈結串列的一端進行,刪除操作在鏈結串列的另一端進行,它表現的特性為先進先出,對應佇列。
    • 雜湊表:鏈式位址是解決雜湊衝突的主流方案之一,在該方案中,所有衝突的元素都會被放到一個鏈結串列中。
    • 圖:鄰接表是表示圖的一種常用方式,其中圖的每個頂點都與一個鏈結串列相關聯,鏈結串列中的每個元素都代表與該頂點相連的其他頂點。

    雙向鏈結串列常用於需要快速查詢前一個和後一個元素的場景。

    • 高階資料結構:比如在紅黑樹、B 樹中,我們需要訪問節點的父節點,這可以透過在節點中儲存一個指向父節點的引用來實現,類似於雙向鏈結串列。
    • 瀏覽器歷史:在網頁瀏覽器中,當用戶點選前進或後退按鈕時,瀏覽器需要知道使用者訪問過的前一個和後一個網頁。雙向鏈結串列的特性使得這種操作變得簡單。
    • LRU 演算法:在快取淘汰(LRU)演算法中,我們需要快速找到最近最少使用的資料,以及支持快速新增和刪除節點。這時候使用雙向鏈結串列就非常合適。

    環形鏈結串列常用於需要週期性操作的場景,比如作業系統的資源排程。

    • 時間片輪轉排程演算法:在作業系統中,時間片輪轉排程演算法是一種常見的 CPU 排程演算法,它需要對一組程序進行迴圈。每個程序被賦予一個時間片,當時間片用完時,CPU 將切換到下一個程序。這種迴圈操作可以透過環形鏈結串列來實現。
    • 資料緩衝區:在某些資料緩衝區的實現中,也可能會使用環形鏈結串列。比如在音訊、影片播放器中,資料流可能會被分成多個緩衝塊並放入一個環形鏈結串列,以便實現無縫播放。
    ","path":["第 4 章   陣列與鏈結串列","4.2   鏈結串列"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   串列","text":"

    串列(list)是一個抽象的資料結構概念,它表示元素的有序集合,支持元素訪問、修改、新增、刪除和走訪等操作,無須使用者考慮容量限制的問題。串列可以基於鏈結串列或陣列實現。

    • 鏈結串列天然可以看作一個串列,其支持元素增刪查改操作,並且可以靈活動態擴容。
    • 陣列也支持元素增刪查改,但由於其長度不可變,因此只能看作一個具有長度限制的串列。

    當使用陣列實現串列時,長度不可變的性質會導致串列的實用性降低。這是因為我們通常無法事先確定需要儲存多少資料,從而難以選擇合適的串列長度。若長度過小,則很可能無法滿足使用需求;若長度過大,則會造成記憶體空間浪費。

    為解決此問題,我們可以使用動態陣列(dynamic array)來實現串列。它繼承了陣列的各項優點,並且可以在程式執行過程中進行動態擴容。

    實際上,許多程式語言中的標準庫提供的串列是基於動態陣列實現的,例如 Python 中的 list 、Java 中的 ArrayList 、C++ 中的 vector 和 C# 中的 List 等。在接下來的討論中,我們將把“串列”和“動態陣列”視為等同的概念。

    ","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// 有初始值(注意陣列的元素型別需為 int[] 的包裝類別 Integer[])\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
    視覺化執行

    全螢幕觀看 >

    ","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
    視覺化執行

    全螢幕觀看 >

    ","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)  # 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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:]...)...) // 在索引 3 處插入數字 6\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) // 在索引 3 處插入數字 6\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); // 在索引 3 處插入數字 6\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); // 在索引 3 處插入數字 6\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); // 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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);  // 在索引 3 處插入數字 6\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) # 在索引 3 處插入數字 6\n\n# 刪除元素\nnums.delete_at(3) # 刪除索引 3 處的元素\n
    視覺化執行

    全螢幕觀看 >

    ","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
    視覺化執行

    全螢幕觀看 >

    ","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
    視覺化執行

    全螢幕觀看 >

    ","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
    視覺化執行

    全螢幕觀看 >

    ","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 提供高速的資料讀取,從而顯著提升程式的執行效率,減少對較慢的記憶體的依賴。

    圖 4-10   硬碟、記憶體和快取之間的資料流通

    ","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:為什麼陣列要求相同型別的元素,而在鏈結串列中卻沒有強調相同型別呢?

    鏈結串列由節點組成,節點之間透過引用(指標)連線,各個節點可以儲存不同型別的資料,例如 intdoublestringobject 等。

    相對地,陣列元素則必須是相同型別的,這樣才能透過計算偏移量來獲取對應元素位置。例如,陣列同時包含 intlong 兩種型別,單個元素分別佔用 4 位元組和 8 位元組 ,此時就不能用以下公式計算偏移量了,因為陣列中包含了兩種“元素長度”。

    # 元素記憶體位址 = 陣列記憶體位址(首元素記憶體位址) + 元素長度 * 元素索引\n

    Q:刪除節點 P 後,是否需要把 P.next 設為 None 呢?

    不修改 P.next 也可以。從該鏈結串列的角度看,從頭節點走訪到尾節點已經不會遇到 P 了。這意味著節點 P 已經從鏈結串列中刪除了,此時節點 P 指向哪裡都不會對該鏈結串列產生影響。

    從資料結構與演算法(做題)的角度看,不斷開沒有關係,只要保證程式的邏輯是正確的就行。從標準庫的角度看,斷開更加安全、邏輯更加清晰。如果不斷開,假設被刪除節點未被正常回收,那麼它會影響後繼節點的記憶體回收。

    Q:在鏈結串列中插入和刪除操作的時間複雜度是 \\(O(1)\\) 。但是增刪之前都需要 \\(O(n)\\) 的時間查詢元素,那為什麼時間複雜度不是 \\(O(n)\\) 呢?

    如果是先查詢元素、再刪除元素,時間複雜度確實是 \\(O(n)\\) 。然而,鏈結串列的 \\(O(1)\\) 增刪的優勢可以在其他應用上得到體現。例如,雙向佇列適合使用鏈結串列實現,我們維護一個指標變數始終指向頭節點、尾節點,每次插入與刪除操作都是 \\(O(1)\\) 。

    Q:圖“鏈結串列定義與儲存方式”中,淺藍色的儲存節點指標是佔用一塊記憶體位址嗎?還是和節點值各佔一半呢?

    該示意圖只是定性表示,定量表示需要根據具體情況進行分析。

    • 不同型別的節點值佔用的空間是不同的,比如 intlongdouble 和例項物件等。
    • 指標變數佔用的記憶體空間大小根據所使用的作業系統及編譯環境而定,大多為 8 位元組或 4 位元組。

    Q:在串列末尾新增元素是否時時刻刻都為 \\(O(1)\\) ?

    如果新增元素時超出串列長度,則需要先擴容串列再新增。系統會申請一塊新的記憶體,並將原串列的所有元素搬運過去,這時候時間複雜度就會是 \\(O(n)\\) 。

    Q:“串列的出現極大地提高了陣列的實用性,但可能導致部分記憶體空間浪費”,這裡的空間浪費是指額外增加的變數如容量、長度、擴容倍數所佔的記憶體嗎?

    這裡的空間浪費主要有兩方面含義:一方面,串列都會設定一個初始長度,我們不一定需要用這麼多;另一方面,為了防止頻繁擴容,擴容一般會乘以一個係數,比如 \\(\\times 1.5\\) 。這樣一來,也會出現很多空位,我們通常不能完全填滿它們。

    Q:在 Python 中初始化 n = [1, 2, 3] 後,這 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 的效能會更好。

    另一方面,必要使用鏈結串列的情況主要是二元樹和圖。堆疊和佇列往往會使用程式語言提供的 stackqueue ,而非鏈結串列。

    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 章   回溯"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"本章內容","text":"
    • 13.1   回溯演算法
    • 13.2   全排列問題
    • 13.3   子集和問題
    • 13.4   N 皇后問題
    • 13.5   小結
    ","path":["第 13 章   回溯"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   回溯演算法","text":"

    回溯演算法(backtracking algorithm)是一種透過窮舉來解決問題的方法,它的核心思想是從一個初始狀態出發,暴力搜尋所有可能的解決方案,當遇到正確的解則將其記錄,直到找到解或者嘗試了所有可能的選擇都無法找到解為止。

    回溯演算法通常採用“深度優先搜尋”來走訪解空間。在“二元樹”章節中,我們提到前序、中序和後序走訪都屬於深度優先搜尋。接下來,我們利用前序走訪構造一個回溯問題,逐步瞭解回溯演算法的工作原理。

    例題一

    給定一棵二元樹,搜尋並記錄所有值為 \\(7\\) 的節點,請返回節點串列。

    對於此題,我們前序走訪這棵樹,並判斷當前節點的值是否為 \\(7\\) ,若是,則將該節點的值加入結果串列 res 之中。相關過程實現如圖 13-1 和以下程式碼所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序走訪:例題一\"\"\"\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    /* 前序走訪:例題一 */\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
    ### 前序走訪:例題一 ###\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":"

    之所以稱之為回溯演算法,是因為該演算法在搜尋解空間時會採用“嘗試”與“回退”的策略。當演算法在搜尋過程中遇到某個狀態無法繼續前進或無法得到滿足條件的解時,它會撤銷上一步的選擇,退回到之前的狀態,並嘗試其他可能的選擇。

    對於例題一,訪問每個節點都代表一次“嘗試”,而越過葉節點或返回父節點的 return 則表示“回退”。

    值得說明的是,回退並不僅僅包括函式返回。為解釋這一點,我們對例題一稍作拓展。

    例題二

    在二元樹中搜索所有值為 \\(7\\) 的節點,請返回根節點到這些節點的路徑。

    在例題一程式碼的基礎上,我們需要藉助一個串列 path 記錄訪問過的節點路徑。當訪問到值為 \\(7\\) 的節點時,則複製 path 並新增進結果串列 res 。走訪完成後,res 中儲存的就是所有的解。程式碼如下所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序走訪:例題二\"\"\"\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    /* 前序走訪:例題二 */\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
    ### 前序走訪:例題二 ###\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":"

    複雜的回溯問題通常包含一個或多個約束條件,約束條件通常可用於“剪枝”。

    例題三

    在二元樹中搜索所有值為 \\(7\\) 的節點,請返回根節點到這些節點的路徑,並要求路徑中不包含值為 \\(3\\) 的節點。

    為了滿足以上約束條件,我們需要新增剪枝操作:在搜尋過程中,若遇到值為 \\(3\\) 的節點,則提前返回,不再繼續搜尋。程式碼如下所示:

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
    def pre_order(root: TreeNode):\n    \"\"\"前序走訪:例題三\"\"\"\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    /* 前序走訪:例題三 */\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
    ### 前序走訪:例題三 ###\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

    接下來,我們基於框架程式碼來解決例題三。狀態 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    \"\"\"回溯演算法:例題三\"\"\"\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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/* 回溯演算法:例題三 */\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### 回溯演算法:例題三 ###\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 的搜尋過程對比

    相比基於前序走訪的程式碼實現,基於回溯演算法框架的程式碼實現雖然顯得囉唆,但通用性更好。實際上,許多回溯問題可以在該框架下解決。我們只需根據具體問題來定義 statechoices ,並實現框架中的各個方法即可。

    ","path":["第 13 章   回溯","13.1   回溯演算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   常用術語","text":"

    為了更清晰地分析演算法問題,我們總結一下回溯演算法中常用術語的含義,並對照例題三給出對應示例,如表 13-1 所示。

    表 13-1   常見的回溯演算法術語

    名詞 定義 例題三 解(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 \\times n\\) 的棋盤上放置 \\(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":"

    為了滿足列約束,我們可以利用一個長度為 \\(n\\) 的布林型陣列 cols 記錄每一列是否有皇后。在每次決定放置前,我們透過 cols 將已有皇后的列進行剪枝,並在回溯中動態更新 cols 的狀態。

    Tip

    請注意,矩陣的起點位於左上角,其中行索引從上到下增加,列索引從左到右增加。

    那麼,如何處理對角線約束呢?設棋盤中某個格子的行列索引為 \\((row, col)\\) ,選定矩陣中的某條主對角線,我們發現該對角線上所有格子的行索引減列索引都相等,即主對角線上所有格子的 \\(row - col\\) 為恆定值。

    也就是說,如果兩個格子滿足 \\(row_1 - col_1 = row_2 - col_2\\) ,則它們一定處在同一條主對角線上。利用該規律,我們可以藉助圖 13-18 所示的陣列 diags1 記錄每條主對角線上是否有皇后。

    同理,次對角線上的所有格子的 \\(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\\) ,即陣列 diags1diags2 的長度都為 \\(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)\\) 空間,陣列 colsdiags1diags2 皆使用 \\(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\\) ,我們將第二個 \\(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":"

    請注意,雖然 selectedduplicated 都用於剪枝,但兩者的目標不同。

    • 重複選擇剪枝:整個搜尋過程中只有一個 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; // 初始化解的數量為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 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    # 剪枝二:從 start 開始走訪,避免生成重複子集\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 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  // 剪枝二:從 start 開始走訪,避免生成重複子集\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超過 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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超過 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  # 剪枝二:從 start 開始走訪,避免生成重複子集\n  for i in start...choices.length\n    # 剪枝一:若子集和超過 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    # 剪枝二:從 start 開始走訪,避免生成重複子集\n    # 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超過 target ,則直接結束迴圈\n        # 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if target - choices[i] < 0:\n            break\n        # 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if target - choices[i] < 0 {\n            break\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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  // 剪枝二:從 start 開始走訪,避免生成重複子集\n  // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超過 target ,則直接結束迴圈\n    // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超過 target ,則直接跳過\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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    // 剪枝二:從 start 開始走訪,避免生成重複子集\n    // 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超過 target ,則直接結束迴圈\n        // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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  # 剪枝二:從 start 開始走訪,避免生成重複子集\n  # 剪枝三:從 start 開始走訪,避免重複選擇同一元素\n  for i in start...choices.length\n    # 剪枝一:若子集和超過 target ,則直接結束迴圈\n    # 這是因為陣列已排序,後邊元素更大,子集和一定超過 target\n    break if target - choices[i] < 0\n    # 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過\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-q-a","level":3,"title":"2.   Q & A","text":"

    Q:怎麼理解回溯和遞迴的關係?

    總的來看,回溯是一種“演算法策略”,而遞迴更像是一個“工具”。

    • 回溯演算法通常基於遞迴實現。然而,回溯是遞迴的應用場景之一,是遞迴在搜尋問題中的應用。
    • 遞迴的結構體現了“子問題分解”的解題範式,常用於解決分治、回溯、動態規劃(記憶化遞迴)等問題。
    ","path":["第 13 章   回溯","13.5   小結"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章   複雜度分析","text":"

    Abstract

    複雜度分析猶如浩瀚的演算法宇宙中的時空嚮導。

    它帶領我們在時間與空間這兩個維度上深入探索,尋找更優雅的解決方案。

    ","path":["第 2 章   複雜度分析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"本章內容","text":"
    • 2.1   演算法效率評估
    • 2.2   迭代與遞迴
    • 2.3   時間複雜度
    • 2.4   空間複雜度
    • 2.5   小結
    ","path":["第 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 迴圈是最常見的迭代形式之一,適合在預先知道迭代次數時使用。

    以下函式基於 for 迴圈實現了求和 \\(1 + 2 + \\dots + n\\) ,求和結果使用變數 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 迴圈中,程式每輪都會先檢查條件,如果條件為真,則繼續執行,否則就結束迴圈。

    下面我們用 while 迴圈來實現求和 \\(1 + 2 + \\dots + n\\) :

    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_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 所示,這樣不斷遞迴呼叫下去,最終將產生一棵層數為 \\(n\\) 的遞迴樹(recursion tree)。

    圖 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() 在迴圈中呼叫了 \\(n\\) 次 function() ,每輪中的 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_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_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 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":"

    演算法效率評估

    • 時間效率和空間效率是衡量演算法優劣的兩個主要評價指標。
    • 我們可以透過實際測試來評估演算法效率,但難以消除測試環境的影響,且會耗費大量計算資源。
    • 複雜度分析可以消除實際測試的弊端,分析結果適用於所有執行平臺,並且能夠揭示演算法在不同資料規模下的效率。

    時間複雜度

    • 時間複雜度用於衡量演算法執行時間隨資料量增長的趨勢,可以有效評估演算法效率,但在某些情況下可能失效,如在輸入的資料量較小或時間複雜度相同時,無法精確對比演算法效率的優劣。
    • 最差時間複雜度使用大 \\(O\\) 符號表示,對應函式漸近上界,反映當 \\(n\\) 趨向正無窮時,操作數量 \\(T(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)與一個物件關聯,被隱式傳遞給呼叫它的物件,能夠對類別的例項中包含的資料進行操作。

    下面以幾種常見的程式語言為例來說明。

    • C 語言是程序式程式設計語言,沒有物件導向的概念,所以只有函式。但我們可以透過建立結構體(struct)來模擬物件導向程式設計,與結構體相關聯的函式就相當於其他程式語言中的方法。
    • Java 和 C# 是物件導向的程式語言,程式碼塊(方法)通常作為某個類別的一部分。靜態方法的行為類似於函式,因為它被繫結在類別上,不能訪問特定的例項變數。
    • C++ 和 Python 既支持程序式程式設計(函式),也支持物件導向程式設計(方法)。

    Q:圖解“常見的空間複雜度型別”反映的是否是佔用空間的絕對大小?

    不是,該圖展示的是空間複雜度,其反映的是增長趨勢,而不是佔用空間的絕對大小。

    假設取 \\(n = 8\\) ,你可能會發現每條曲線的值與函式對應不上。這是因為每條曲線都包含一個常數項,用於將取值範圍壓縮到一個視覺舒適的範圍內。

    在實際中,因為我們通常不知道每個方法的“常數項”複雜度是多少,所以一般無法僅憑複雜度來選擇 \\(n = 8\\) 之下的最優解法。但對於 \\(n = 8^5\\) 就很好選了,這時增長趨勢已經佔主導了。

    Q 是否存在根據實際使用場景,選擇犧牲時間(或空間)來設計演算法的情況?

    在實際應用中,大部分情況會選擇犧牲空間換時間。例如資料庫索引,我們通常選擇建立 B+ 樹或雜湊索引,佔用大量記憶體空間,以換取 \\(O(\\log n)\\) 甚至 \\(O(1)\\) 的高效查詢。

    在空間資源寶貴的場景,也會選擇犧牲時間換空間。例如在嵌入式開發中,裝置記憶體很寶貴,工程師可能會放棄使用雜湊表,選擇使用陣列順序查詢,以節省記憶體佔用,代價是查詢變慢。

    ","path":["第 2 章   複雜度分析","2.5   小結"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   時間複雜度","text":"

    執行時間可以直觀且準確地反映演算法的效率。如果我們想準確預估一段程式碼的執行時間,應該如何操作呢?

    1. 確定執行平臺,包括硬體配置、程式語言、系統環境等,這些因素都會影響程式碼的執行效率。
    2. 評估各種計算操作所需的執行時間,例如加法操作 + 需要 1 ns ,乘法操作 * 需要 10 ns ,列印操作 print() 需要 5 ns 等。
    3. 統計程式碼中所有的計算操作,並將所有操作的執行時間求和,從而得到執行時間。

    例如在以下程式碼中,輸入資料大小為 \\(n\\) :

    PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
    # 在某執行平臺下\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # 迴圈 n 次\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
    // 在某執行平臺下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 迴圈 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
    // 在某執行平臺下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 迴圈 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
    // 在某執行平臺下\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 迴圈 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // 迴圈 n 次\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 迴圈 n 次\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 迴圈 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 迴圈 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
    // 在某執行平臺下\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // 迴圈 n 次\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
    // 在某執行平臺下\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // 迴圈 n 次\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
    // 在某執行平臺下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 迴圈 n 次\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
    // 在某執行平臺下\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 迴圈 n 次\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
    # 在某執行平臺下\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # 迴圈 n 次\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

    根據以上方法,可以得到演算法的執行時間為 \\((6n + 12)\\) ns :

    \\[ 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\\) ,給定三個演算法 ABC

    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 只有 \\(1\\) 個列印操作,演算法執行時間不隨著 \\(n\\) 增大而增長。我們稱此演算法的時間複雜度為“常數階”。
    • 演算法 B 中的列印操作需要迴圈 \\(n\\) 次,演算法執行時間隨著 \\(n\\) 增大呈線性增長。此演算法的時間複雜度被稱為“線性階”。
    • 演算法 C 中的列印操作需要迴圈 \\(1000000\\) 次,雖然執行時間很長,但它與輸入資料大小 \\(n\\) 無關。因此 C 的時間複雜度和 A 相同,仍為“常數階”。

    圖 2-7   演算法 A、B 和 C 的時間增長趨勢

    相較於直接統計演算法的執行時間,時間複雜度分析有哪些特點呢?

    • 時間複雜度能夠有效評估演算法效率。例如,演算法 B 的執行時間呈線性增長,在 \\(n > 1\\) 時比演算法 A 更慢,在 \\(n > 1000000\\) 時比演算法 C 更慢。事實上,只要輸入資料大小 \\(n\\) 足夠大,複雜度為“常數階”的演算法一定優於“線性階”的演算法,這正是時間增長趨勢的含義。
    • 時間複雜度的推算方法更簡便。顯然,執行平臺和計算操作型別都與演算法執行時間的增長趨勢無關。因此在時間複雜度分析中,我們可以簡單地將所有計算操作的執行時間視為相同的“單位時間”,從而將“計算操作執行時間統計”簡化為“計算操作數量統計”,這樣一來估算難度就大大降低了。
    • 時間複雜度也存在一定的侷限性。例如,儘管演算法 AC 的時間複雜度相同,但實際執行時間差別很大。同樣,儘管演算法 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)\\) ,這個數學符號稱為大 \\(O\\) 記號(big-\\(O\\) notation),表示函式 \\(T(n)\\) 的漸近上界(asymptotic upper bound)。

    時間複雜度分析本質上是計算“操作數量 \\(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","level":3,"title":"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","level":3,"title":"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 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\\) 為輸入資料大小;在第二個示例中,陣列長度 \\(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 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":"

    生物學的“細胞分裂”是指數階增長的典型例子:初始狀態為 \\(1\\) 個細胞,分裂一輪後變為 \\(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 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 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 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 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    # 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 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  // 從 1 個分裂出 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    // 從 1 個分裂出 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    // 從 1 個分裂出 n 個\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
    time_complexity.rb
    ### 階乘階(遞迴實現)###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # 從 1 個分裂出 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":"

    演算法的時間效率往往不是固定的,而是與輸入資料的分佈有關。假設輸入一個長度為 \\(n\\) 的陣列 nums ,其中 nums 由從 \\(1\\) 至 \\(n\\) 的數字組成,每個數字只出現一次;但元素順序是隨機打亂的,任務目標是返回元素 \\(1\\) 的索引。我們可以得出以下結論。

    • nums = [?, ?, ..., 1] ,即當末尾元素是 \\(1\\) 時,需要完整走訪陣列,達到最差時間複雜度 \\(O(n)\\) 。
    • nums = [1, ?, ?, ...] ,即當首個元素為 \\(1\\) 時,無論陣列多長都不需要繼續走訪,達到最佳時間複雜度 \\(\\Omega(1)\\) 。

    “最差時間複雜度”對應函式漸近上界,使用大 \\(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    \"\"\"查詢陣列 nums 中數字 1 所在索引\"\"\"\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    // 使用系統時間生成隨機種子\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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/* 查詢陣列 nums 中數字 1 所在索引 */\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### 查詢陣列 nums 中數字 1 所在索引 ###\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 章   資料結構"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"本章內容","text":"
    • 3.1   資料結構分類
    • 3.2   基本資料型別
    • 3.3   數字編碼 *
    • 3.4   字元編碼 *
    • 3.5   小結
    ","path":["第 3 章   資料結構"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   基本資料型別","text":"

    當談及計算機中的資料時,我們會想到文字、圖片、影片、語音、3D 模型等各種形式。儘管這些資料的組織形式各異,但它們都由各種基本資料型別構成。

    基本資料型別是 CPU 可以直接進行運算的型別,在演算法中直接被使用,主要包括以下幾種。

    • 整數型別 byteshortintlong
    • 浮點數型別 floatdouble ,用於表示小數。
    • 字元型別 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 型別,單個字元實際上是長度為 1 的字串 str
    • C 和 C++ 未明確規定基本資料型別的大小,而因實現和平臺各異。表 3-1 遵循 LP64 資料模型,其用於包括 Linux 和 macOS 在內的 Unix 64 位作業系統。
    • 字元 char 的大小在 C 和 C++ 中為 1 位元組,在大多數程式語言中取決於特定的字元編碼方法,詳見“字元編碼”章節。
    • 即使表示布林量僅需 1 位(\\(0\\) 或 \\(1\\)),它在記憶體中通常也儲存為 1 位元組。這是因為現代計算機 CPU 通常將 1 位元組作為最小定址記憶體單元。

    那麼,基本資料型別與資料結構之間有什麼關聯呢?我們知道,資料結構是在計算機中組織與儲存資料的方式。這句話的主語是“結構”而非“資料”。

    如果想表示“一排數字”,我們自然會想到使用陣列。這是因為陣列的線性結構可以表示數字的相鄰關係和順序關係,但至於儲存的內容是整數 int、小數 float 還是字元 char ,則與“資料結構”無關。

    換句話說,基本資料型別提供了資料的“內容型別”,而資料結構提供了資料的“組織方式”。例如以下程式碼,我們用相同的資料結構(陣列)來儲存與表示不同的基本資料型別,包括 intfloatcharbool 等。

    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
    視覺化執行

    全螢幕觀看 >

    ","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 字元集。它在 ASCII 的 7 位基礎上擴展到 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 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字元集之中,提供一種通用的字元集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。自 1991 年釋出以來,Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月,Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。

    Unicode 作為一種通用字元集,本質上是給每個字元分配唯一的“碼點”(字元編號),其取值範圍為 U+0000 至 U+10FFFF,構成了統一的字元編號空間。然而,Unicode 並沒有規定在計算機中如何儲存這些字元碼點。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元?

    對於以上問題,一種直接的解決方案是將所有字元儲存為等長的編碼。如圖 3-7 所示,“Hello”中的每個字元佔用 1 位元組,“演算法”中的每個字元佔用 2 位元組。我們可以透過高位填 0 將“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 字元在 Unicode 字元集中佔據了前 128 個碼點。也就是說,UTF-8 編碼可以向下相容 ASCII 碼。這意味著我們可以使用 UTF-8 來解析年代久遠的 ASCII 碼文字。
    • 對於長度為 \\(n\\) 位元組的字元(其中 \\(n > 1\\)),將首個位元組的高 \\(n\\) 位都設定為 \\(1\\) ,第 \\(n + 1\\) 位設定為 \\(0\\) ;從第二個位元組開始,將每個位元組的高 2 位都設定為 \\(10\\) ;其餘所有位用於填充字元的 Unicode 碼點。

    圖 3-8 展示了“Hello演算法”對應的 UTF-8 編碼。觀察發現,由於最高 \\(n\\) 位都設定為 \\(1\\) ,因此系統可以透過讀取最高位 \\(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\\) 個字元,我們需要從字串的開始處走訪到第 \\(i\\) 個字元,這需要 \\(O(n)\\) 的時間。
    • 字元計數:與隨機訪問類似,計算 UTF-16 編碼的字串的長度也是 \\(O(1)\\) 的操作。但是,計算 UTF-8 編碼的字串的長度需要走訪整個字串。
    • 字串操作:在 UTF-16 編碼的字串上,很多字串操作(如分割、連線、插入、刪除等)更容易進行。在 UTF-8 編碼的字串上,進行這些操作通常需要額外的計算,以確保不會產生無效的 UTF-8 編碼。

    實際上,程式語言的字元編碼方案設計是一個很有趣的話題,涉及許多因素。

    • Java 的 String 型別使用 UTF-16 編碼,每個字元佔用 2 位元組。這是因為 Java 語言設計之初,人們認為 16 位足以表示所有可能的字元。然而,這是一個不正確的判斷。後來 Unicode 規範擴展到了超過 16 位,所以 Java 中的字元現在可能由一對 16 位的值(稱為“代理對”)表示。
    • JavaScript 和 TypeScript 的字串使用 UTF-16 編碼的原因與 Java 類似。當 1995 年 Netscape 公司首次推出 JavaScript 語言時,Unicode 還處於發展早期,那時候使用 16 位的編碼就足以表示所有的 Unicode 字元了。
    • C# 使用 UTF-16 編碼,主要是因為 .NET 平臺是由 Microsoft 設計的,而 Microsoft 的很多技術(包括 Windows 作業系統)都廣泛使用 UTF-16 編碼。

    由於以上程式語言對字元數量的低估,它們不得不採取“代理對”的方式來表示超過 16 位長度的 Unicode 字元。這是一個不得已為之的無奈之舉。一方面,包含代理對的字串中,一個字元可能佔用 2 位元組或 4 位元組,從而喪失了等長編碼的優勢。另一方面,處理代理對需要額外增加程式碼,這提高了程式設計的複雜性和除錯難度。

    出於以上原因,部分程式語言提出了一些不同的編碼方案。

    • Python 中的 str 使用 Unicode 編碼,並採用一種靈活的字串表示,儲存的字元長度取決於字串中最大的 Unicode 碼點。若字串中全部是 ASCII 字元,則每個字元佔用 1 位元組;如果有字元超出了 ASCII 範圍,但全部在基本多語言平面(BMP)內,則每個字元佔用 2 位元組;如果有超出 BMP 的字元,則每個字元佔用 4 位元組。
    • Go 語言的 string 型別在內部使用 UTF-8 編碼。Go 語言還提供了 rune 型別,它用於表示單個 Unicode 碼點。
    • Rust 語言的 strString 型別在內部使用 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 位的 \\(1\\) 會被捨棄。也就是說,負零的二補數為 \\(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":"

    細心的你可能會發現:intfloat 長度相同,都是 4 位元組 ,但為什麼 float 的取值範圍遠大於 int ?這非常反直覺,因為按理說 float 需要表示小數,取值範圍應該變小才對。

    實際上,這是因為浮點數 float 採用了不同的表示方式。記一個 32 位元長度的二進位制數為:

    \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

    根據 IEEE 754 標準,32-bit 長度的 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   IEEE 754 標準下的 float 的計算示例

    觀察圖 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":"
    • 資料結構可以從邏輯結構和物理結構兩個角度進行分類。邏輯結構描述了資料元素之間的邏輯關係,而物理結構描述了資料在計算機記憶體中的儲存方式。
    • 常見的邏輯結構包括線性、樹狀和網狀等。通常我們根據邏輯結構將資料結構分為線性(陣列、鏈結串列、堆疊、佇列)和非線性(樹、圖、堆積)兩種。雜湊表的實現可能同時包含線性資料結構和非線性資料結構。
    • 當程式執行時,資料被儲存在計算機記憶體中。每個記憶體空間都擁有對應的記憶體位址,程式透過這些記憶體位址訪問資料。
    • 物理結構主要分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。所有資料結構都是由陣列、鏈結串列或兩者的組合實現的。
    • 計算機中的基本資料型別包括整數 byteshortintlong ,浮點數 floatdouble ,字元 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:基於陣列實現的資料結構也稱“靜態資料結構” 是否有歧義?堆疊也可以進行出堆疊和入堆疊等操作,這些操作都是“動態”的。

    堆疊確實可以實現動態的資料操作,但資料結構仍然是“靜態”(長度不可變)的。儘管基於陣列的資料結構可以動態地新增或刪除元素,但它們的容量是固定的。如果資料量超出了預分配的大小,就需要建立一個新的更大的陣列,並將舊陣列的內容複製到新陣列中。

    Q:在構建堆疊(佇列)的時候,未指定它的大小,為什麼它們是“靜態資料結構”呢?

    在高階程式語言中,我們無須人工指定堆疊(佇列)的初始容量,這個工作由類別內部自動完成。例如,Java 的 ArrayList 的初始容量通常為 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 章   分治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"本章內容","text":"
    • 12.1   分治演算法
    • 12.2   分治搜尋策略
    • 12.3   構建樹問題
    • 12.4   河內塔問題
    • 12.5   小結
    ","path":["第 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

    給定一個長度為 \\(n\\) 的有序陣列 nums ,其中所有元素都是唯一的,請查詢元素 target

    從分治角度,我們將搜尋區間 \\([i, j]\\) 對應的子問題記為 \\(f(i, j)\\) 。

    以原問題 \\(f(0, n-1)\\) 為起始點,透過以下步驟進行二分搜尋。

    1. 計算搜尋區間 \\([i, j]\\) 的中點 \\(m\\) ,根據它排除一半搜尋區間。
    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":"

    原問題定義為從 preorderinorder 構建二元樹,是一個典型的分治問題。

    • 問題可以分解:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構建左子樹、構建右子樹,加上一步操作:初始化根節點。而對於每棵子樹(子問題),我們仍然可以複用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
    • 子問題是獨立的:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構建左子樹時,我們只需關注中序走訪和前序走訪中與左子樹對應的部分。右子樹同理。
    • 子問題的解可以合併:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們連結到根節點上,得到原問題的解。
    ","path":["第 12 章   分治","12.3   構建二元樹問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   如何劃分子樹","text":"

    根據以上分析,這道題可以使用分治來求解,但如何透過前序走訪 preorder 和中序走訪 inorder 來劃分左子樹和右子樹呢?

    根據定義,preorderinorder 都可以劃分為三個部分。

    • 前序走訪:[ 根節點 | 左子樹 | 右子樹 ] ,例如圖 12-5 的樹對應 [ 3 | 9 | 2 1 7 ]
    • 中序走訪:[ 左子樹 | 根節點 | 右子樹 ] ,例如圖 12-5 的樹對應 [ 9 | 3 | 1 2 7 ]

    以上圖資料為例,我們可以透過圖 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":"

    根據以上劃分方法,我們已經得到根節點、左子樹、右子樹在 preorderinorder 中的索引區間。而為了描述這些索引區間,我們需要藉助幾個指標變數。

    • 將當前樹的根節點在 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":"

    一方面,分治可以用來解決許多經典演算法問題。

    • 尋找最近點對:該演算法首先將點集分成兩部分,然後分別找出兩部分中的最近點對,最後找出跨越兩部分的最近點對。
    • 大整數乘法:例如 Karatsuba 演算法,它將大整數乘法分解為幾個較小的整數的乘法和加法。
    • 矩陣乘法:例如 Strassen 演算法,它將大矩陣乘法分解為多個小矩陣的乘法和加法。
    • 河內塔問題:河內塔問題可以透過遞迴解決,這是典型的分治策略應用。
    • 求解逆序對:在一個序列中,如果前面的數字大於後面的數字,那麼這兩個數字構成一個逆序對。求解逆序對問題可以利用分治的思想,藉助合併排序進行求解。

    另一方面,分治在演算法和資料結構的設計中應用得非常廣泛。

    • 二分搜尋:二分搜尋是將有序陣列從中點索引處分為兩部分,然後根據目標值與中間元素值比較結果,決定排除哪一半區間,並在剩餘區間執行相同的二分操作。
    • 合併排序:本節開頭已介紹,不再贅述。
    • 快速排序:快速排序是選取一個基準值,然後把陣列分為兩個子陣列,一個子陣列的元素比基準值小,另一子陣列的元素比基準值大,再對這兩部分進行相同的劃分操作,直至子陣列只剩下一個元素。
    • 桶排序:桶排序的基本思想是將資料分散到多個桶,然後對每個桶內的元素進行排序,最後將各個桶的元素依次取出,從而得到一個有序陣列。
    • 樹:例如二元搜尋樹、AVL 樹、紅黑樹、B 樹、B+ 樹等,它們的查詢、插入和刪除等操作都可以視為分治策略的應用。
    • 堆積:堆積是一種特殊的完全二元樹,其各種操作,如插入、刪除和堆積化,實際上都隱含了分治的思想。
    • 雜湊表:雖然雜湊表並不直接應用分治,但某些雜湊衝突解決方案間接應用了分治策略,例如,鏈式位址中的長鏈結串列會被轉化為紅黑樹,以提升查詢效率。

    可以看出,分治是一種“潤物細無聲”的演算法思想,隱含在各種演算法與資料結構之中。

    ","path":["第 12 章   分治","12.1   分治演算法"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   河內塔問題","text":"

    在合併排序和構建二元樹中,我們都是將原問題分解為兩個規模為原問題一半的子問題。然而對於河內塔問題,我們採用不同的分解策略。

    Question

    給定三根柱子,記為 ABC 。起始狀態下,柱子 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)\\) 的過程可總結為:將兩個圓盤藉助 BA 移至 C 。其中,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 中剩餘的一個圓盤從 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\\) 個圓盤藉助 CA 移至 B
    2. 將剩餘 \\(1\\) 個圓盤從 A 直接移至 C
    3. 將 \\(n-1\\) 個圓盤藉助 AB 移至 C

    對於這兩個子問題 \\(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) ,它的作用是將柱 src 頂部的 \\(i\\) 個圓盤藉助緩衝柱 buf 移動至目標柱 tar

    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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    # 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar)\n    # 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    # 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    DFS(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    Move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfsHanota(i-1, src, tar, buf)\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar)\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* 求解河內塔問題 */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\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) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 src 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\n    dfs(i - 1, buf, src, tar);\n}\n\n/* 求解河內塔問題 */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n  dfs(i - 1, src, tar, buf);\n  // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n  move(src, tar);\n  // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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  // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move_pan(src, tar);\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\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) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    // 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n    move(src, tar)\n    // 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\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    // 將 A 頂部 n 個圓盤藉助 B 移到 C\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) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf\n  dfs(i - 1, src, tar, buf)\n  # 子問題 f(1) :將 src 剩餘一個圓盤移到 tar\n  move(src, tar)\n  # 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar\n  dfs(i - 1, buf, src, tar)\nend\n\n### 求解河內塔問題 ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # 將 A 頂部 n 個圓盤藉助 B 移到 C\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}\\) 秒,合約 \\(5850\\) 億年,遠遠超過了現在對宇宙年齡的估計。所以,倘若這個傳說是真的,我們應該不需要擔心世界末日的到來。

    ","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 章   動態規劃"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"本章內容","text":"
    • 14.1   初探動態規劃
    • 14.2   DP 問題特性
    • 14.3   DP 解題思路
    • 14.4   0-1 背包問題
    • 14.5   完全背包問題
    • 14.6   編輯距離問題
    • 14.7   小結
    ","path":["第 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\\) ,則從地面爬到第 \\(3\\) 階的最小代價為 \\(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 所示,爬上第 \\(3\\) 階僅剩 \\(2\\) 種可行方案,其中連續三次跳 \\(1\\) 階的方案不滿足約束條件,因此被捨棄。

    圖 14-8   帶約束爬到第 3 階的方案數量

    在該問題中,如果上一輪是跳 \\(1\\) 階上來的,那麼下一輪就必須跳 \\(2\\) 階。這意味著,下一步選擇不能由當前狀態(當前所在樓梯階數)獨立決定,還和前一個狀態(上一輪所在樓梯階數)有關。

    不難發現,此問題已不滿足無後效性,狀態轉移方程 \\(dp[i] = dp[i-1] + dp[i-2]\\) 也失效了,因為 \\(dp[i-1]\\) 代表本輪跳 \\(1\\) 階,但其中包含了許多“上一輪是跳 \\(1\\) 階上來的”方案,而為了滿足約束,我們就不能將 \\(dp[i-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

    給定一個 \\(n \\times m\\) 的二維網格 grid ,網格中的每個單元格包含一個非負整數,表示該單元格的代價。機器人以左上角單元格為起始點,每次只能向下或者向右移動一步,直至到達右下角單元格。請返回從左上角到右下角的最小路徑和。

    圖 14-10 展示了一個例子,給定網格的最小路徑和為 \\(13\\) 。

    圖 14-10   最小路徑和示例資料

    第一步:思考每輪的決策,定義狀態,從而得到 \\(dp\\) 表

    本題的每一輪的決策就是從當前格子向下或向右走一步。設當前格子的行列索引為 \\([i, j]\\) ,則向下或向右走一步後,索引變為 \\([i+1, j]\\) 或 \\([i, j+1]\\) 。因此,狀態應包含行索引和列索引兩個變數,記為 \\([i, j]\\) 。

    狀態 \\([i, j]\\) 對應的子問題為:從起始點 \\([0, 0]\\) 走到 \\([i, j]\\) 的最小路徑和,解記為 \\(dp[i, j]\\) 。

    至此,我們就得到了圖 14-11 所示的二維 \\(dp\\) 矩陣,其尺寸與輸入網格 \\(grid\\) 相同。

    圖 14-11   狀態定義與 dp 表

    Note

    動態規劃和回溯過程可以描述為一個決策序列,而狀態由所有決策變數構成。它應當包含描述解題進度的所有變數,其包含了足夠的資訊,能夠用來推導出下一個狀態。

    每個狀態都對應一個子問題,我們會定義一個 \\(dp\\) 表來儲存所有子問題的解,狀態的每個獨立變數都是 \\(dp\\) 表的一個維度。從本質上看,\\(dp\\) 表是狀態和子問題的解之間的對映。

    第二步:找出最優子結構,進而推導出狀態轉移方程

    對於狀態 \\([i, j]\\) ,它只能從上邊格子 \\([i-1, j]\\) 和左邊格子 \\([i, j-1]\\) 轉移而來。因此最優子結構為:到達 \\([i, j]\\) 的最小路徑和由 \\([i, j-1]\\) 的最小路徑和與 \\([i-1, j]\\) 的最小路徑和中較小的那一個決定。

    根據以上分析,可推出圖 14-12 所示的狀態轉移方程:

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

    圖 14-12   最優子結構與狀態轉移方程

    Note

    根據定義好的 \\(dp\\) 表,思考原問題和子問題的關係,找出透過子問題的最優解來構造原問題的最優解的方法,即最優子結構。

    一旦我們找到了最優子結構,就可以使用它來構建出狀態轉移方程。

    第三步:確定邊界條件和狀態轉移順序

    在本題中,處在首行的狀態只能從其左邊的狀態得來,處在首列的狀態只能從其上邊的狀態得來,因此首行 \\(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","level":3,"title":"1.   方法一:暴力搜尋","text":"

    從狀態 \\([i, j]\\) 開始搜尋,不斷分解為更小的狀態 \\([i-1, j]\\) 和 \\([i, j-1]\\) ,遞迴函式包括以下要素。

    • 遞迴參數:狀態 \\([i, j]\\) 。
    • 返回值:從 \\([0, 0]\\) 到 \\([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","level":3,"title":"2.   方法二:記憶化搜尋","text":"

    我們引入一個和網格 grid 相同尺寸的記憶串列 mem ,用於記錄各個子問題的解,並將重疊子問題進行剪枝:

    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","level":3,"title":"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":"

    編輯距離,也稱 Levenshtein 距離,指兩個字串之間互相轉換的最少修改次數,通常用於在資訊檢索和自然語言處理中度量兩個序列的相似度。

    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":"

    第一步:思考每輪的決策,定義狀態,從而得到 \\(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, j]\\) 對應的子問題:將 \\(s\\) 的前 \\(i\\) 個字元更改為 \\(t\\) 的前 \\(j\\) 個字元所需的最少編輯步數。

    至此,得到一個尺寸為 \\((i+1) \\times (j+1)\\) 的二維 \\(dp\\) 表。

    第二步:找出最優子結構,進而推導出狀態轉移方程

    考慮子問題 \\(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] \\]

    第三步:確定邊界條件和狀態轉移順序

    當兩字串都為空時,編輯步數為 \\(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-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","level":2,"title":"14.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 -1\\) 階或第 \\(i - 2\\) 階邁向第 \\(i\\) 階。

    由此便可得出一個重要推論:爬到第 \\(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","level":2,"title":"14.1.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","level":2,"title":"14.1.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\\) 對應子問題的解。
    • 將最小子問題對應的狀態(第 \\(1\\) 階和第 \\(2\\) 階樓梯)稱為初始狀態。
    • 將遞推公式 \\(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\\) 輪決策組成的過程,對於每個物體都有不放入和放入兩種決策,因此該問題滿足決策樹模型。

    該問題的目標是求解“在限定背包容量下能放入物品的最大價值”,因此較大機率是一個動態規劃問題。

    第一步:思考每輪的決策,定義狀態,從而得到 \\(dp\\) 表

    對於每個物品來說,不放入背包,背包容量不變;放入背包,背包容量減小。由此可得狀態定義:當前物品編號 \\(i\\) 和背包容量 \\(c\\) ,記為 \\([i, c]\\) 。

    狀態 \\([i, c]\\) 對應的子問題為:前 \\(i\\) 個物品在容量為 \\(c\\) 的背包中的最大價值,記為 \\(dp[i, c]\\) 。

    待求解的是 \\(dp[n, cap]\\) ,因此需要一個尺寸為 \\((n+1) \\times (cap+1)\\) 的二維 \\(dp\\) 表。

    第二步:找出最優子結構,進而推導出狀態轉移方程

    當我們做出物品 \\(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\\) ,則只能選擇不放入背包。

    第三步:確定邊界條件和狀態轉移順序

    當無物品或背包容量為 \\(0\\) 時最大價值為 \\(0\\) ,即首列 \\(dp[i, 0]\\) 和首行 \\(dp[0, c]\\) 都等於 \\(0\\) 。

    當前狀態 \\([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","level":3,"title":"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","level":3,"title":"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","level":3,"title":"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 的第一維 \\(i\\) 直接刪除,並且把內迴圈更改為倒序走訪即可:

    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()\\) 改為求和運算子。

    編輯距離問題

    • 編輯距離(Levenshtein 距離)用於衡量兩個字串之間的相似度,其定義為從一個字串到另一個字串的最少編輯步數,編輯操作包括新增、刪除、替換。
    • 編輯距離問題的狀態定義為將 \\(s\\) 的前 \\(i\\) 個字元更改為 \\(t\\) 的前 \\(j\\) 個字元所需的最少編輯步數。當 \\(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":"

    對比兩道題目的程式碼,狀態轉移中有一處從 \\(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":"

    零錢兌換可以看作完全背包問題的一種特殊情況,兩者具有以下關聯與不同點。

    • 兩道題可以相互轉換,“物品”對應“硬幣”、“物品重量”對應“硬幣面值”、“背包容量”對應“目標金額”。
    • 最佳化目標相反,完全背包問題是要最大化物品價值,零錢兌換問題是要最小化硬幣數量。
    • 完全背包問題是求“不超過”背包容量下的解,零錢兌換是求“恰好”湊到目標金額的解。

    第一步:思考每輪的決策,定義狀態,從而得到 \\(dp\\) 表

    狀態 \\([i, a]\\) 對應的子問題為:前 \\(i\\) 種硬幣能夠湊出金額 \\(a\\) 的最少硬幣數量,記為 \\(dp[i, a]\\) 。

    二維 \\(dp\\) 表的尺寸為 \\((n+1) \\times (amt+1)\\) 。

    第二步:找出最優子結構,進而推導出狀態轉移方程

    本題與完全背包問題的狀態轉移方程存在以下兩點差異。

    • 本題要求最小值,因此需將運算子 \\(\\max()\\) 更改為 \\(\\min()\\) 。
    • 最佳化主體是硬幣數量而非商品價值,因此在選中硬幣時執行 \\(+1\\) 即可。
    \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

    第三步:確定邊界條件和狀態轉移順序

    當目標金額為 \\(0\\) 時,湊出它的最少硬幣數量為 \\(0\\) ,即首列所有 \\(dp[i, 0]\\) 都等於 \\(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]\\) 都初始化為 \\(1\\) 。當無硬幣時,無法湊出任何 \\(>0\\) 的目標金額,因此首行所有 \\(dp[0, a]\\) 都等於 \\(0\\) 。

    ","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 章   圖"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"本章內容","text":"
    • 9.1   圖
    • 9.2   圖基礎操作
    • 9.3   圖的走訪
    • 9.4   小結
    ","path":["第 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 所示。

    • 在無向圖中,邊表示兩頂點之間的“雙向”連線關係,例如微信或 QQ 中的“好友關係”。
    • 在有向圖中,邊具有方向性,即 \\(A \\rightarrow B\\) 和 \\(A \\leftarrow B\\) 兩個方向的邊是相互獨立的,例如微博或抖音上的“關注”與“被關注”關係。

    圖 9-2   有向圖與無向圖

    根據所有頂點是否連通,可分為連通圖(connected graph)和非連通圖(disconnected graph),如圖 9-3 所示。

    • 對於連通圖,從某個頂點出發,可以到達其餘任意頂點。
    • 對於非連通圖,從某個頂點出發,至少有一個頂點無法到達。

    圖 9-3   連通圖與非連通圖

    我們還可以為邊新增“權重”變數,從而得到如圖 9-4 所示的有權圖(weighted graph)。例如在《王者榮耀》等手遊中,系統會根據共同遊戲時間來計算玩家之間的“親密度”,這種親密度網路就可以用有權圖來表示。

    圖 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\\) 大小的矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 \\(1\\) 或 \\(0\\) 表示兩個頂點之間是否存在邊。

    如圖 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)\\) 時間。而由於是無向圖,因此需要同時更新兩個方向的邊。
    • 新增頂點:在鄰接矩陣的尾部新增一行一列,並全部填 \\(0\\) 即可,使用 \\(O(n)\\) 時間。
    • 刪除頂點:在鄰接矩陣中刪除一行一列。當刪除首行首列時達到最差情況,需要將 \\((n-1)^2\\) 個元素“向左上移動”,從而使用 \\(O(n^2)\\) 時間。
    • 初始化:傳入 \\(n\\) 個頂點,初始化長度為 \\(n\\) 的頂點串列 vertices ,使用 \\(O(n)\\) 時間;初始化 \\(n \\times n\\) 大小的鄰接矩陣 adjMat ,使用 \\(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\\) 的頂點,則需走訪整個鄰接表,將所有大於 \\(i\\) 的索引全部減 \\(1\\) ,效率很低。而如果每個頂點都是唯一的 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 的雜湊表,它可以在 \\(O(1)\\) 時間複雜度下進行 key 的增刪查改操作。根據 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 操作會返回新的的引用,必須讓原引用重新賦值為新slice的引用\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   圖的深度優先走訪步驟

    深度優先走訪的序列是否唯一?

    與廣度優先走訪類似,深度優先走訪序列的順序也不是唯一的。給定某頂點,先往哪個方向探索都可以,即鄰接頂點的順序可以任意打亂,都是深度優先走訪。

    以樹的走訪為例,“根 \\(\\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":"
    • 圖由頂點和邊組成,可以表示為一組頂點和一組邊構成的集合。
    • 相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)具有更高的自由度,因而更為複雜。
    • 有向圖的邊具有方向性,連通圖中的任意頂點均可達,有權圖的每條邊都包含權重變數。
    • 鄰接矩陣利用矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 \\(1\\) 或 \\(0\\) 表示兩個頂點之間有邊或無邊。鄰接矩陣在增刪查改操作上效率很高,但空間佔用較多。
    • 鄰接表使用多個鏈結串列來表示圖,第 \\(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 章   貪婪"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"本章內容","text":"
    • 15.1   貪婪演算法
    • 15.2   分數背包問題
    • 15.3   最大容量問題
    • 15.4   最大切分乘積問題
    • 15.5   小結
    ","path":["第 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(\\log n)\\) ,空間複雜度通常為 \\(O(\\log n)\\) 或 \\(O(n)\\) ,取決於程式語言的具體實現。

    除排序之外,在最差情況下,需要走訪整個物品串列,因此時間複雜度為 \\(O(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
    視覺化執行

    全螢幕觀看 >

    你可能會不由地發出感嘆:So clean !貪婪演算法僅用約十行程式碼就解決了零錢兌換問題。

    ","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":"

    貪婪演算法常常應用在滿足貪婪選擇性質和最優子結構的最佳化問題中,以下列舉了一些典型的貪婪演算法問題。

    • 硬幣找零問題:在某些硬幣組合下,貪婪演算法總是可以得到最優解。
    • 區間排程問題:假設你有一些任務,每個任務在一段時間內進行,你的目標是完成儘可能多的任務。如果每次都選擇結束時間最早的任務,那麼貪婪演算法就可以得到最優解。
    • 分數背包問題:給定一組物品和一個載重量,你的目標是選擇一組物品,使得總重量不超過載重量,且總價值最大。如果每次都選擇價效比最高(價值 / 重量)的物品,那麼貪婪演算法在一些情況下可以得到最優解。
    • 股票買賣問題:給定一組股票的歷史價格,你可以進行多次買賣,但如果你已經持有股票,那麼在賣出之前不能再買,目標是獲取最大利潤。
    • 霍夫曼編碼:霍夫曼編碼是一種用於無損資料壓縮的貪婪演算法。透過構建霍夫曼樹,每次選擇出現頻率最低的兩個節點合併,最後得到的霍夫曼樹的帶權路徑長度(編碼長度)最小。
    • Dijkstra 演算法:它是一種解決給定源頂點到其餘各頂點的最短路徑問題的貪婪演算法。
    ","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\\) 的整數都應該被切分。

    貪婪策略一:如果切分方案中包含 \\(\\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\\) 總是可以替換為兩個 \\(3\\) ,從而獲得更大的乘積。

    圖 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 所示,我們無須透過迴圈來切分整數,而可以利用向下整除運算得到 \\(3\\) 的個數 \\(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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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 為 3 的個數,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() 內部呼叫 C 語言庫的 pow() 函式,其執行浮點取冪,時間複雜度為 \\(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\\) :假設最優切分方案中存在 \\(\\geq 4\\) 的因子 \\(x\\) ,那麼一定可以將其繼續劃分為 \\(2(x-2)\\) ,從而獲得更大(或相等)的乘積。這與假設矛盾。
    2. 切分方案不包含 \\(1\\) :假設最優切分方案中存在一個因子 \\(1\\) ,那麼它一定可以合併入另外一個因子中,以獲得更大的乘積。這與假設矛盾。
    3. 切分方案最多包含兩個 \\(2\\) :假設最優切分方案中包含三個 \\(2\\) ,那麼一定可以替換為兩個 \\(3\\) ,乘積更大。這與假設矛盾。
    ","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 章   雜湊表"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"本章內容","text":"
    • 6.1   雜湊表
    • 6.2   雜湊衝突
    • 6.3   雜湊演算法
    • 6.4   小結
    ","path":["第 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 碼累積到雜湊值中。
    • 互斥或雜湊:將輸入資料的每個元素透過互斥或操作累積到一個雜湊值中。
    • 旋轉雜湊:將每個字元的 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    \"\"\"互斥或雜湊\"\"\"\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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/* 互斥或雜湊 */\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### 互斥或雜湊 ###\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\\) 整除,那麼所有可以被 \\(3\\) 整除的 key 都會被對映到 \\(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\\) ,由於 keymodulus 之間不存在公約數,因此輸出的雜湊值的均勻性會明顯提升。

    \\[ \\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":"

    不難發現,以上介紹的簡單雜湊演算法都比較“脆弱”,遠遠沒有達到雜湊演算法的設計目標。例如,由於加法和互斥或滿足交換律,因此加法雜湊和互斥或雜湊無法區分內容相同但順序不同的字串,這可能會加劇雜湊衝突,並引起一些安全問題。

    在實際中,我們通常會用一些標準雜湊演算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它們可以將任意長度的輸入資料對映到恆定長度的雜湊值。

    近一個世紀以來,雜湊演算法處在不斷升級與最佳化的過程中。一部分研究人員努力提升雜湊演算法的效能,另一部分研究人員和駭客則致力於尋找雜湊演算法的安全性問題。表 6-2 展示了在實際應用中常見的雜湊演算法。

    • MD5 和 SHA-1 已多次被成功攻擊,因此它們被各類安全應用棄用。
    • SHA-2 系列中的 SHA-256 是最安全的雜湊演算法之一,仍未出現成功的攻擊案例,因此常用在各類安全應用與協議中。
    • 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 演算法\"\nhash_str = hash(str)\n# 字串“Hello 演算法”的雜湊值為 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 演算法\";\nsize_t hashStr = hash<string>()(str);\n// 字串“Hello 演算法”的雜湊值為 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 演算法\";\nint hashStr = str.hashCode();\n// 字串“Hello 演算法”的雜湊值為 -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 演算法\";\nint hashStr = str.GetHashCode();\n// 字串“Hello 演算法”的雜湊值為 -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 演算法\"\nlet hashStr = str.hashValue\n// 字串“Hello 演算法”的雜湊值為 -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 演算法\";\nint hashStr = str.hashCode;\n// 字串“Hello 演算法”的雜湊值為 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 演算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 字串“Hello 演算法”的雜湊值為 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 演算法\"\nval hashStr = str.hashCode()\n// 字串“Hello 演算法”的雜湊值為 -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 演算法\"\nhash_str = str.hash\n# 字串“Hello 演算法”的雜湊值為 -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
    視覺化執行

    全螢幕觀看 >

    在許多程式語言中,只有不可變物件才可作為雜湊表的 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 來標記這個桶。在該機制下,NoneTOMBSTONE 都代表空桶,都可以放置鍵值對。但不同的是,線性探查到 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 個鍵值對,超出容量則連線一個溢位桶;當溢位桶過多時,會執行一次特殊的等量擴容操作,以確保效能。
    ","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
    視覺化執行

    全螢幕觀看 >

    雜湊表有三種常用的走訪方式:走訪鍵值對、走訪鍵和走訪值。示例程式碼如下:

    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
    視覺化執行

    全螢幕觀看 >

    ","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 對應的鍵值對在陣列中的儲存位置。

    輸入一個 key ,雜湊函式的計算過程分為以下兩步。

    1. 透過某種雜湊演算法 hash() 計算得到雜湊值。
    2. 將雜湊值對桶數量(陣列長度)capacity 取模,從而獲取該 key 對應的桶(陣列索引)index
    index = hash(key) % capacity\n

    隨後,我們就可以利用 index 在雜湊表中訪問對應的桶,從而獲取 value

    設陣列長度 capacity = 100、雜湊演算法 hash(key) = key ,易得雜湊函式為 key % 100 。圖 6-2 以 key 學號和 value 姓名為例,展示了雜湊函式的工作原理。

    圖 6-2   雜湊函式工作原理

    以下程式碼實現了一個簡單雜湊表。其中,我們將 keyvalue 封裝成一個類別 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 ,雜湊表能夠在 \\(O(1)\\) 時間內查詢到 value ,效率非常高。
    • 常見的雜湊表操作包括查詢、新增鍵值對、刪除鍵值對和走訪雜湊表等。
    • 雜湊函式將 key 對映為陣列索引,從而訪問對應桶並獲取 value
    • 兩個不同的 key 可能在經過雜湊函式後得到相同的陣列索引,導致查詢結果出錯,這種現象被稱為雜湊衝突。
    • 雜湊表容量越大,雜湊衝突的機率就越低。因此可以透過擴容雜湊表來緩解雜湊衝突。與陣列擴容類似,雜湊表擴容操作的開銷很大。
    • 負載因子定義為雜湊表中元素數量除以桶數量,反映了雜湊衝突的嚴重程度,常用作觸發雜湊表擴容的條件。
    • 鏈式位址透過將單個元素轉化為鏈結串列,將所有衝突元素儲存在同一個鏈結串列中。然而,鏈結串列過長會降低查詢效率,可以透過進一步將鏈結串列轉換為紅黑樹來提高效率。
    • 開放定址透過多次探測來處理雜湊衝突。線性探查使用固定步長,缺點是不能刪除元素,且容易產生聚集。多次雜湊使用多個雜湊函式進行探測,相較線性探查更不易產生聚集,但多個雜湊函式增加了計算量。
    • 不同程式語言採取了不同的雜湊表實現。例如,Java 的 HashMap 使用鏈式位址,而 Python 的 Dict 採用開放定址。
    • 在雜湊表中,我們希望雜湊演算法具有確定性、高效率和均勻分佈的特點。在密碼學中,雜湊演算法還應該具備抗碰撞性和雪崩效應。
    • 雜湊演算法通常採用大質數作為模數,以最大化地保證雜湊值均勻分佈,減少雜湊衝突。
    • 常見的雜湊演算法包括 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 章   堆積"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"本章內容","text":"
    • 8.1   堆積
    • 8.2   建堆積操作
    • 8.3   Top-k 問題
    • 8.4   小結
    ","path":["第 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    # 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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        // 堆積化除葉節點以外的其他所有節點\n        h.siftDown(i)\n    }\n    return h\n}\n
    my_heap.swift
    /* 建構子,根據輸入串列建堆積 */\ninit(nums: [Int]) {\n    // 將串列元素原封不動新增進堆積\n    maxHeap = nums\n    // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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  // 堆積化除葉節點以外的其他所有節點\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    // 堆積化除葉節點以外的其他所有節點\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        // 堆積化除葉節點以外的其他所有節點\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        // 堆積化除葉節點以外的其他所有節點\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            // 當“越過根節點”或“節點無須修復”時,結束堆積化\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  # 堆積化除葉節點以外的其他所有節點\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)\\) 。但這個估算結果並不準確,因為我們沒有考慮到二元樹底層節點數量遠多於頂層節點的性質。

    接下來我們來進行更為準確的計算。為了降低計算難度,假設給定一個節點數量為 \\(n\\) 、高度為 \\(h\\) 的“完美二元樹”,該假設不會影響計算結果的正確性。

    圖 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() 訪問堆積頂元素(對於大 / 小頂堆積分別為最大 / 小值) \\(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# Python 的 heapq 模組預設實現小頂堆積\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// 初始化大頂堆積(使用 lambda 表示式修改 Comparator 即可)\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// 初始化大頂堆積(使用 lambda 表示式修改 Comparer 即可)\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// Swift 的 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// 初始化大頂堆積(使用 lambda 表示式修改 Comparator 即可)\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
    視覺化執行

    全螢幕觀看 >

    ","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 可能大於堆積中其他元素,堆積的成立條件可能已被破壞,因此需要修復從插入節點到根節點的路徑上的各個節點,這個操作被稱為堆積化(heapify)。

    考慮從入堆積節點開始,從底至頂執行堆積化。如圖 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        # 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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    // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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        // 當“越過根節點”或“節點無須修復”時,結束堆積化\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    # 當“越過根節點”或“節點無須修復”時,結束堆積化\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

    給定一個長度為 \\(n\\) 的無序陣列 nums ,請返回陣列中最大的 \\(k\\) 個元素。

    對於該問題,我們先介紹兩種思路比較直接的解法,再介紹效率更高的堆積解法。

    ","path":["第 8 章   堆積","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#831","level":2,"title":"8.3.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","level":2,"title":"8.3.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","level":2,"title":"8.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":"

    幾年前,我在力扣上分享了“劍指 Offer”系列題解,受到了許多讀者的鼓勵和支持。在與讀者交流期間,我最常被問的一個問題是“如何入門演算法”。逐漸地,我對這個問題產生了濃厚的興趣。

    兩眼一抹黑地刷題似乎是最受歡迎的方法,簡單、直接且有效。然而刷題就如同玩“掃雷”遊戲,自學能力強的人能夠順利將地雷逐個排掉,而基礎不足的人很可能被炸得滿頭是包,並在挫折中步步退縮。通讀教材也是一種常見做法,但對於面向求職的人來說,畢業論文、投遞簡歷、準備筆試和面試已經消耗了大部分精力,啃厚重的書往往變成了一項艱鉅的挑戰。

    如果你也面臨類似的困擾,那麼很幸運這本書“找”到了你。本書是我對這個問題給出的答案,即使不是最優解,也至少是一次積極的嘗試。本書雖然不足以讓你直接拿到 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 章   初識演算法"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"本章內容","text":"
    • 1.1   演算法無處不在
    • 1.2   演算法是什麼
    • 1.3   小結
    ","path":["第 1 章   初識演算法"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   演算法無處不在","text":"

    當我們聽到“演算法”這個詞時,很自然地會想到數學。然而實際上,許多演算法並不涉及複雜數學,而是更多地依賴基本邏輯,這些邏輯在我們的日常生活中處處可見。

    在正式探討演算法之前,有一個有趣的事實值得分享:你已經在不知不覺中學會了許多演算法,並習慣將它們應用到日常生活中了。下面我將舉幾個具體的例子來證實這一點。

    例一:查字典。在字典裡,每個漢字都對應一個拼音,而字典是按照拼音字母順序排列的。假設我們需要查詢一個拼音首字母為 \\(r\\) 的字,通常會按照圖 1-1 所示的方式實現。

    1. 翻開字典約一半的頁數,檢視該頁的首字母是什麼,假設首字母為 \\(m\\) 。
    2. 由於在拼音字母表中 \\(r\\) 位於 \\(m\\) 之後,所以排除字典前半部分,查詢範圍縮小到後半部分。
    3. 不斷重複步驟 1. 和步驟 2. ,直至找到拼音首字母為 \\(r\\) 的頁碼為止。
    <1><2><3><4><5>

    圖 1-1   查字典步驟

    查字典這個小學生必備技能,實際上就是著名的“二分搜尋”演算法。從資料結構的角度,我們可以把字典視為一個已排序的“陣列”;從演算法的角度,我們可以將上述查字典的一系列操作看作“二分搜尋”。

    例二:整理撲克。我們在打牌時,每局都需要整理手中的撲克牌,使其從小到大排列,實現流程如圖 1-2 所示。

    1. 將撲克牌劃分為“有序”和“無序”兩部分,並假設初始狀態下最左 1 張撲克牌已經有序。
    2. 在無序部分抽出一張撲克牌,插入至有序部分的正確位置;完成後最左 2 張撲克已經有序。
    3. 不斷迴圈步驟 2. ,每一輪將一張撲克牌從無序部分插入至有序部分,直至所有撲克牌都有序。

    圖 1-2   撲克排序步驟

    上述整理撲克牌的方法本質上是“插入排序”演算法,它在處理小型資料集時非常高效。許多程式語言的排序庫函式中都有插入排序的身影。

    例三:貨幣找零。假設我們在超市購買了 \\(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 章   前言"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"本章內容","text":"
    • 0.1   關於本書
    • 0.2   如何使用本書
    • 0.3   小結
    ","path":["第 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 開發。感謝他的貢獻,為讀者提供了更靈活的閱讀方式。

    在本書的創作過程中,我得到了許多人的幫助。

    • 感謝我在公司的導師李汐博士,在一次暢談中你鼓勵我“快行動起來”,堅定了我寫這本書的決心;
    • 感謝我的女朋友泡泡作為本書的首位讀者,從演算法小白的角度提出許多寶貴建議,使得本書更適合新手閱讀;
    • 感謝騰寶、琦寶、飛寶為本書起了一個富有創意的名字,喚起大家寫下第一行程式碼“Hello World!”的美好回憶;
    • 感謝校銓在智慧財產權方面提供的專業幫助,這對本開源書的完善起到了重要作用;
    • 感謝蘇潼為本書設計了精美的封面和 logo ,並在我的強迫症的驅使下多次耐心修改;
    • 感謝 @squidfunk 提供的排版建議,以及他開發的開源文件主題 Material-for-MkDocs 。

    在寫作過程中,我閱讀了許多關於資料結構與演算法的教材和文章。這些作品為本書提供了優秀的範本,確保了本書內容的準確性與品質。在此感謝所有老師和前輩的傑出貢獻!

    本書倡導手腦並用的學習方式,在這一點上我深受《動手學深度學習》的啟發。在此向各位讀者強烈推薦這本優秀的著作。

    衷心感謝我的父母,正是你們一直以來的支持與鼓勵,讓我有機會做這件富有趣味的事。

    ","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   執行程式碼示例

    執行程式碼的前置工作主要分為三步。

    第一步:安裝本地程式設計環境。請參照附錄所示的教程進行安裝,如果已安裝,則可跳過此步驟。

    第二步:克隆或下載程式碼倉庫。前往 GitHub 倉庫。如果已經安裝 Git ,可以透過以下命令克隆本倉庫:

    git clone https://github.com/krahets/hello-algo.git\n

    當然,你也可以在圖 0-4 所示的位置,點選“Download ZIP”按鈕直接下載程式碼壓縮包,然後在本地解壓即可。

    圖 0-4   克隆倉庫與下載程式碼

    第三步:執行源程式碼。如圖 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. 階段一:演算法入門。我們需要熟悉各種資料結構的特點和用法,學習不同演算法的原理、流程、用途和效率等方面的內容。
    2. 階段二:刷演算法題。建議從熱門題目開刷,先積累至少 100 道題目,熟悉主流的演算法問題。初次刷題時,“知識遺忘”可能是一個挑戰,但請放心,這是很正常的。我們可以按照“艾賓浩斯遺忘曲線”來複習題目,通常在進行 3~5 輪的重複後,就能將其牢記在心。推薦的題單和刷題計劃請見此 GitHub 倉庫。
    3. 階段三:搭建知識體系。在學習方面,我們可以閱讀演算法專欄文章、解題框架和演算法教材,以不斷豐富知識體系。在刷題方面,可以嘗試採用進階刷題策略,如按專題分類、一題多解、一解多題等,相關的刷題心得可以在各個社群找到。

    如圖 0-8 所示,本書內容主要涵蓋“階段一”,旨在幫助你更高效地展開階段二和階段三的學習。

    圖 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, et al. Introduction to Algorithms (3rd Edition).

    [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

    [3] Robert Sedgewick, et al. Algorithms (4th Edition).

    [4] 嚴蔚敏. 資料結構(C 語言版).

    [5] 鄧俊輝. 資料結構(C++ 語言版,第三版).

    [6] 馬克 艾倫 維斯著,陳越譯. 資料結構與演算法分析:Java語言描述(第三版).

    [7] 程傑. 大話資料結構.

    [8] 王爭. 資料結構與演算法之美.

    [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

    [10] Aston Zhang, et al. Dive into Deep Learning.

    ","path":["參考文獻"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章   搜尋","text":"

    Abstract

    搜尋是一場未知的冒險,我們或許需要走遍神秘空間的每個角落,又或許可以快速鎖定目標。

    在這場尋覓之旅中,每一次探索都可能得到一個未曾料想的答案。

    ","path":["第 10 章   搜尋"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"本章內容","text":"
    • 10.1   二分搜尋
    • 10.2   二分搜尋插入點
    • 10.3   二分搜尋邊界
    • 10.4   雜湊最佳化策略
    • 10.5   重識搜尋演算法
    • 10.6   小結
    ","path":["第 10 章   搜尋"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   二分搜尋","text":"

    二分搜尋(binary search)是一種基於分治策略的高效搜尋演算法。它利用資料的有序性,每輪縮小一半搜尋範圍,直至找到目標元素或搜尋區間為空為止。

    Question

    給定一個長度為 \\(n\\) 的陣列 nums ,元素按從小到大的順序排列且不重複。請查詢並返回元素 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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 分別指向陣列首元素、尾元素+1\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 次除法、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

    給定一個長度為 \\(n\\) 的有序陣列 nums ,其中可能包含重複元素。請返回陣列中最左一個元素 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 的元素。

    因此,如圖 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

    給定一個長度為 \\(n\\) 的有序陣列 nums 和一個元素 target ,陣列不存在重複元素。現將 target 插入陣列 nums 中,並保持其有序性。若陣列中已存在元素 target ,則插入到其左方。請返回插入後 target 在陣列中的索引。示例如圖 10-4 所示。

    圖 10-4   二分搜尋插入點示例資料

    如果想複用上一節的二分搜尋程式碼,則需要回答以下兩個問題。

    問題一:當陣列中包含 target 時,插入點的索引是否是該元素的索引?

    題目要求將 target 插入到相等元素的左邊,這意味著新插入的 target 替換了原來 target 的位置。也就是說,當陣列包含 target 時,插入點的索引就是該 target 的索引。

    問題二:當陣列中不存在 target 時,插入點是哪個元素的索引?

    進一步思考二分搜尋過程:當 nums[m] < target 時 \\(i\\) 移動,這意味著指標 \\(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

    題目要求將目標元素插入到最左邊,所以我們需要查詢陣列中最左一個 target 的索引。初步考慮透過圖 10-5 所示的步驟實現。

    1. 執行二分搜尋,得到任意一個 target 的索引,記為 \\(k\\) 。
    2. 從索引 \\(k\\) 開始,向左進行線性走訪,當找到最左邊的 target 時返回。

    圖 10-5   線性查詢重複元素的插入點

    此方法雖然可用,但其包含線性查詢,因此時間複雜度為 \\(O(n)\\) 。當陣列中存在很多重複的 target 時,該方法效率很低。

    現考慮拓展二分搜尋程式碼。如圖 10-6 所示,整體流程保持不變,每輪先計算中點索引 \\(m\\) ,再判斷 targetnums[m] 的大小關係,分為以下幾種情況。

    • nums[m] < targetnums[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] > targetnums[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    \"\"\"方法一:暴力列舉\"\"\"\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一: 暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    /* 方法一:暴力列舉 */\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
    ### 方法一:暴力列舉 ###\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    \"\"\"方法二:輔助雜湊表\"\"\"\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    /* 方法二: 輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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/* 方法二:輔助雜湊表 */\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
    /* 方法二:輔助雜湊表 */\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
    ### 方法二:輔助雜湊表 ###\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 章   排序"],"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 章   排序"],"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_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":"

    桶排序適用於處理體量很大的資料。例如,輸入資料包含 100 萬個元素,由於空間限制,系統記憶體無法一次性載入所有資料。此時,可以將資料分成 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 + 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":"

    先來看一個簡單的例子。給定一個長度為 \\(n\\) 的陣列 nums ,其中的元素都是“非負整數”,計數排序的整體流程如圖 11-16 所示。

    1. 走訪陣列,找出其中的最大數字,記為 \\(m\\) ,然後建立一個長度為 \\(m + 1\\) 的輔助陣列 counter
    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    # 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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  // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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    // 使用結果陣列 res 覆蓋原陣列 nums\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  # 使用結果陣列 res 覆蓋原陣列 nums\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)\\)、非原地排序:藉助了長度分別為 \\(n\\) 和 \\(m\\) 的陣列 rescounter
    • 穩定排序:由於向 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. 從堆積頂元素開始,從頂到底執行堆積化操作(sift down)。完成堆積化後,堆積的性質得到修復。
    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    # 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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  // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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    // 建堆積操作:堆積化除葉節點以外的其他所有節點\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  # 建堆積操作:堆積化除葉節點以外的其他所有節點\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. 初始狀態下,陣列的第 1 個元素已完成排序。
    2. 選取陣列的第 2 個元素作為 base ,將其插入到正確位置後,陣列的前 2 個元素已排序。
    3. 選取第 3 個元素作為 base ,將其插入到正確位置後,陣列的前 3 個元素已排序。
    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. 選取陣列最左端元素作為基準數,初始化兩個指標 ij 分別指向陣列的兩端。
    2. 設定一個迴圈,在每輪中使用 ij)分別尋找第一個比基準數大(小)的元素,然後交換這兩個元素。
    3. 迴圈執行步驟 2. ,直到 ij 相遇時停止,最後將基準數交換至兩個子陣列的分界線。
    <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 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 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   基數排序演算法流程

    下面剖析程式碼實現。對於一個 \\(d\\) 進位制的數字 \\(x\\) ,要獲取其第 \\(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    \"\"\"獲取元素 num 的第 k 位,其中 exp = 10^(k-1)\"\"\"\n    # 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"計數排序(根據 nums 第 k 位排序)\"\"\"\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)  # 獲取 nums[i] 第 k 位,記為 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  # 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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) // 獲取 nums[i] 第 k 位,記為 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 // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    (num / exp) % 10\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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) // 獲取 nums[i] 第 k 位,記為 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 // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 _num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n  return (_num ~/ exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return ((num / exp) % 10) as usize;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10;\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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); // 初始化為 0 以支持後續記憶體釋放\n    // 統計 0~9 各數字的出現次數\n    for (int i = 0; i < size; i++) {\n        // 獲取 nums[i] 第 k 位,記為 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; // 獲取 d 在陣列中的索引 j\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
    /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n    return (num / exp) % 10\n}\n\n/* 計數排序(根據 nums 第 k 位排序) */\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) // 獲取 nums[i] 第 k 位,記為 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 // 獲取 d 在陣列中的索引 j\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
    ### 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) ###\ndef digit(num, exp)\n  # 轉入 exp 而非 k 可以避免在此重複執行昂貴的次方計算\n  (num / exp) % 10\nend\n\n### 計數排序(根據 nums 第 k 位排序)###\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) # 獲取 nums[i] 第 k 位,記為 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 # 獲取 d 在陣列中的索引 j\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)\\)、非原地排序:與計數排序相同,基數排序需要藉助長度為 \\(n\\) 和 \\(d\\) 的陣列 rescounter
    • 穩定排序:當計數排序穩定時,基數排序也穩定;當計數排序不穩定時,基數排序無法保證得到正確的排序結果。
    ","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\\) 處的元素交換。完成後,陣列前 1 個元素已排序。
    3. 選取區間 \\([1, n-1]\\) 中的最小元素,將其與索引 \\(1\\) 處的元素交換。完成後,陣列前 2 個元素已排序。
    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":"

    執行效率:我們期望排序演算法的時間複雜度儘量低,且總體操作數量較少(時間複雜度中的常數項變小)。對於大資料量的情況,執行效率顯得尤為重要。

    就地性:顧名思義,原地排序透過在原陣列上直接操作實現排序,無須藉助額外的輔助陣列,從而節省記憶體。通常情況下,原地排序的資料搬運操作較少,執行速度也更快。

    穩定性:穩定排序在完成排序後,相等元素在陣列中的相對順序不發生改變。

    穩定排序是多級排序場景的必要條件。假設我們有一個儲存學生資訊的表格,第 1 列和第 2 列分別是姓名和年齡。在這種情況下,非穩定排序可能導致輸入資料的有序性喪失:

    # 輸入資料是按照姓名排序好的\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-q-a","level":3,"title":"2.   Q & A","text":"

    Q:排序演算法穩定性在什麼情況下是必需的?

    在現實中,我們有可能基於物件的某個屬性進行排序。例如,學生有姓名和身高兩個屬性,我們希望實現一個多級排序:先按照姓名進行排序,得到 (A, 180) (B, 185) (C, 170) (D, 170) ;再對身高進行排序。由於排序演算法不穩定,因此可能得到 (D, 170) (C, 170) (A, 180) (B, 185)

    可以發現,學生 D 和 C 的位置發生了交換,姓名的有序性被破壞了,而這是我們不希望看到的。

    Q:哨兵劃分中“從右往左查詢”與“從左往右查詢”的順序可以交換嗎?

    不行,當我們以最左端元素為基準數時,必須先“從右往左查詢”再“從左往右查詢”。這個結論有些反直覺,我們來剖析一下原因。

    哨兵劃分 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] 為基準數,那麼正好反過來,必須先“從左往右查詢”。

    Q:關於快速排序的遞迴深度最佳化,為什麼選短的陣列能保證遞迴深度不超過 \\(\\log n\\) ?

    遞迴深度就是當前未返回的遞迴方法的數量。每輪哨兵劃分我們將原陣列劃分為兩個子陣列。在遞迴深度最佳化後,向下遞迴的子陣列長度最大為原陣列長度的一半。假設最差情況,一直為一半長度,那麼最終的遞迴深度就是 \\(\\log n\\) 。

    回顧原始的快速排序,我們有可能會連續地遞迴長度較大的陣列,最差情況下為 \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) ,遞迴深度為 \\(n\\) 。遞迴深度最佳化可以避免這種情況出現。

    Q:當陣列中所有元素都相等時,快速排序的時間複雜度是 \\(O(n^2)\\) 嗎?該如何處理這種退化情況?

    是的。對於這種情況,可以考慮透過哨兵劃分將陣列劃分為三個部分:小於、等於、大於基準數。僅向下遞迴小於和大於的兩部分。在該方法下,輸入元素全部相等的陣列,僅一輪哨兵劃分即可完成排序。

    Q:桶排序的最差時間複雜度為什麼是 \\(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 章   堆疊與佇列"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"本章內容","text":"
    • 5.1   堆疊
    • 5.2   佇列
    • 5.3   雙向佇列
    • 5.4   小結
    ","path":["第 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
    視覺化執行

    全螢幕觀看 >

    ","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":"

    回顧上一節內容,我們使用普通單向鏈結串列來實現佇列,因為它可以方便地刪除頭節點(對應出列操作)和在尾節點後新增新節點(對應入列操作)。

    對於雙向佇列而言,頭部和尾部都可以執行入列和出列操作。換句話說,雙向佇列需要實現另一個對稱方向的操作。為此,我們採用“雙向鏈結串列”作為雙向佇列的底層資料結構。

    如圖 5-8 所示,我們將雙向鏈結串列的頭節點和尾節點視為雙向佇列的佇列首和佇列尾,同時實現在兩端新增和刪除節點的功能。

    <1><2><3><4><5>

    圖 5-8   基於鏈結串列實現雙向佇列的入列出列操作

    實現程式碼如下所示:

    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   基於陣列實現雙向佇列的入列出列操作

    在佇列的實現基礎上,僅需增加“佇列首入列”和“佇列尾出列”的方法:

    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;        // 佇列首指標,指向佇列首元素\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; // 佇列首指標,指向佇列首元素\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;   // 佇列首指標,指向佇列首元素\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   // 佇列首指標,指向佇列首元素\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 // 佇列首指標,指向佇列首元素\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; // 佇列首指標,指向佇列首元素\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; // 佇列首指標,指向佇列首元素\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; // 佇列首指標,指向佇列首元素\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,    // 佇列首指標,指向佇列首元素\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;       // 佇列首指標,指向佇列首元素\n    int queSize;     // 尾指標,指向佇列尾 + 1\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 // 佇列首指標,指向佇列首元素\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":"

    雙向佇列兼具堆疊與佇列的邏輯,因此它可以實現這兩者的所有應用場景,同時提供更高的自由度。

    我們知道,軟體的“撤銷”功能通常使用堆疊來實現:系統將每次更改操作 push 到堆疊中,然後透過 pop 實現撤銷。然而,考慮到系統資源的限制,軟體通常會限制撤銷的步數(例如僅允許儲存 \\(50\\) 步)。當堆疊的長度超過 \\(50\\) 時,軟體需要在堆疊底(佇列首)執行刪除操作。但堆疊無法實現該功能,此時就需要使用雙向佇列來替代堆疊。請注意,“撤銷”的核心邏輯仍然遵循堆疊的先入後出原則,只是雙向佇列能夠更加靈活地實現一些額外邏輯。

    ","path":["第 5 章   堆疊與佇列","5.3   雙向佇列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   佇列","text":"

    佇列(queue)是一種遵循先入先出規則的線性資料結構。顧名思義,佇列模擬了排隊現象,即新來的人不斷加入佇列尾部,而位於佇列頭部的人逐個離開。

    如圖 5-4 所示,我們將佇列頭部稱為“佇列首”,尾部稱為“佇列尾”,將把元素加入列尾的操作稱為“入列”,刪除佇列首元素的操作稱為“出列”。

    圖 5-4   佇列的先入先出規則

    ","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 中,佇列類別 Qeque 是雙向佇列,也可作為佇列使用\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 和走訪方法,可以把 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
    視覺化執行

    全螢幕觀看 >

    ","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   基於鏈結串列實現佇列的入列出列操作

    以下是用鏈結串列實現佇列的程式碼:

    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        # 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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    // 如果佇列為空,則令頭、尾節點都指向該節點\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            // 如果佇列為空,則令頭、尾節點都指向該節點\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    // 如果佇列為空,則令頭、尾節點都指向該節點\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        // 如果佇列為空,則令頭、尾節點都指向該節點\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    # 如果佇列為空,則令頭,尾節點都指向該節點\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)\\) ,這會導致出列操作效率較低。然而,我們可以採用以下巧妙方法來避免這個問題。

    我們可以使用一個變數 front 指向佇列首元素的索引,並維護一個變數 size 用於記錄佇列長度。定義 rear = front + size ,這個公式計算出的 rear 指向佇列尾元素之後的下一個位置。

    基於此設計,陣列中包含元素的有效區間為 [front, rear - 1],各種操作的實現方法如圖 5-6 所示。

    • 入列操作:將輸入元素賦值給 rear 索引處,並將 size 增加 1 。
    • 出列操作:只需將 front 增加 1 ,並將 size 減少 1 。

    可以看到,入列和出列操作都只需進行一次操作,時間複雜度均為 \\(O(1)\\) 。

    <1><2><3>

    圖 5-6   基於陣列實現佇列的入列出列操作

    你可能會發現一個問題:在不斷進行入列和出列的過程中,frontrear 都在向右移動,當它們到達陣列尾部時就無法繼續移動了。為了解決此問題,我們可以將陣列視為首尾相接的“環形陣列”。

    對於環形陣列,我們需要讓 frontrear 在越過陣列尾部時,直接回到陣列頭部繼續走訪。這種週期性規律可以透過“取餘操作”來實現,程式碼如下所示:

    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  # 佇列首指標,指向佇列首元素\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        # 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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;       // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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; // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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;   // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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   // 佇列首指標,指向佇列首元素\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    // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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 // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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; // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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; // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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; // 佇列首指標,指向佇列首元素\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    // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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,        // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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;       // 佇列首指標,指向佇列首元素\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    // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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 // 佇列首指標,指向佇列首元素\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        // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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 # 佇列首指標,指向佇列首元素\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    # 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部\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 所示,我們把堆積疊元素的頂部稱為“堆疊頂”,底部稱為“堆疊底”。將把元素新增到堆疊頂的操作叫作“入堆疊”,刪除堆疊頂元素的操作叫作“出堆疊”。

    圖 5-1   堆疊的先入後出規則

    ","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
    視覺化執行

    全螢幕觀看 >

    ","path":["第 5 章   堆疊與佇列","5.1   堆疊"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   堆疊的實現","text":"

    為了深入瞭解堆疊的執行機制,我們來嘗試自己實現一個堆疊類別。

    堆疊遵循先入後出的原則,因此我們只能在堆疊頂新增或刪除元素。然而,陣列和鏈結串列都可以在任意位置新增和刪除元素,因此堆疊可以視為一種受限制的陣列或鏈結串列。換句話說,我們可以“遮蔽”陣列或鏈結串列的部分無關操作,使其對外表現的邏輯符合堆疊的特性。

    ","path":["第 5 章   堆疊與佇列","5.1   堆疊"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   基於鏈結串列的實現","text":"

    使用鏈結串列實現堆疊時,我們可以將鏈結串列的頭節點視為堆疊頂,尾節點視為堆疊底。

    如圖 5-2 所示,對於入堆疊操作,我們只需將元素插入鏈結串列頭部,這種節點插入方法被稱為“頭插法”。而對於出堆疊操作,只需將頭節點從鏈結串列中刪除即可。

    <1><2><3>

    圖 5-2   基於鏈結串列實現堆疊的入堆疊出堆疊操作

    以下是基於鏈結串列實現堆疊的示例程式碼:

    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 所示,入堆疊與出堆疊操作分別對應在陣列尾部新增元素與刪除元素,時間複雜度都為 \\(O(1)\\) 。

    <1><2><3>

    圖 5-3   基於陣列實現堆疊的入堆疊出堆疊操作

    由於入堆疊的元素可能會源源不斷地增加,因此我們可以使用動態陣列,這樣就無須自行處理陣列擴容問題。以下為示例程式碼:

    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":"

    支持操作

    兩種實現都支持堆疊定義中的各項操作。陣列實現額外支持隨機訪問,但這已超出了堆疊的定義範疇,因此一般不會用到。

    時間效率

    在基於陣列的實現中,入堆疊和出堆疊操作都在預先分配好的連續記憶體中進行,具有很好的快取本地性,因此效率較高。然而,如果入堆疊時超出陣列容量,會觸發擴容機制,導致該次入堆疊操作的時間複雜度變為 \\(O(n)\\) 。

    在基於鏈結串列的實現中,鏈結串列的擴容非常靈活,不存在上述陣列擴容時效率降低的問題。但是,入堆疊操作需要初始化節點物件並修改指標,因此效率相對較低。不過,如果入堆疊元素本身就是節點物件,那麼可以省去初始化步驟,從而提高效率。

    綜上所述,當入堆疊與出堆疊操作的元素是基本資料型別時,例如 intdouble ,我們可以得出以下結論。

    • 基於陣列實現的堆疊在觸發擴容時效率會降低,但由於擴容是低頻操作,因此平均效率更高。
    • 基於鏈結串列實現的堆疊可以提供更加穩定的效率表現。

    空間效率

    在初始化串列時,系統會為串列分配“初始容量”,該容量可能超出實際需求;並且,擴容機制通常是按照特定倍率(例如 2 倍)進行擴容的,擴容後的容量也可能超出實際需求。因此,基於陣列實現的堆疊可能造成一定的空間浪費。

    然而,由於鏈結串列節點需要額外儲存指標,因此鏈結串列節點佔用的空間相對較大。

    綜上,我們不能簡單地確定哪種實現更加節省記憶體,需要針對具體情況進行分析。

    ","path":["第 5 章   堆疊與佇列","5.1   堆疊"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   堆疊的典型應用","text":"
    • 瀏覽器中的後退與前進、軟體中的撤銷與反撤銷。每當我們開啟新的網頁,瀏覽器就會對上一個網頁執行入堆疊,這樣我們就可以通過後退操作回到上一個網頁。後退操作實際上是在執行出堆疊。如果要同時支持後退和前進,那麼需要兩個堆疊來配合實現。
    • 程式記憶體管理。每次呼叫函式時,系統都會在堆疊頂新增一個堆疊幀,用於記錄函式的上下文資訊。在遞迴函式中,向下遞推階段會不斷執行入堆疊操作,而向上回溯階段則會不斷執行出堆疊操作。
    ","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":"
    • 堆疊是一種遵循先入後出原則的資料結構,可透過陣列或鏈結串列來實現。
    • 在時間效率方面,堆疊的陣列實現具有較高的平均效率,但在擴容過程中,單次入堆疊操作的時間複雜度會劣化至 \\(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:在出堆疊後,是否需要釋放出堆疊節點的記憶體?

    如果後續仍需要使用彈出節點,則不需要釋放記憶體。若之後不需要用到,JavaPython 等語言擁有自動垃圾回收機制,因此不需要手動釋放記憶體;在 CC++ 中需要手動釋放記憶體。

    Q:雙向佇列像是兩個堆疊拼接在了一起,它的用途是什麼?

    雙向佇列就像是堆疊和佇列的組合或兩個堆疊拼在了一起。它表現的是堆疊 + 佇列的邏輯,因此可以實現堆疊與佇列的所有應用,並且更加靈活。

    Q:撤銷(undo)和反撤銷(redo)具體是如何實現的?

    使用兩個堆疊,堆疊 A 用於撤銷,堆疊 B 用於反撤銷。

    1. 每當使用者執行一個操作,將這個操作壓入堆疊 A ,並清空堆疊 B
    2. 當用戶執行“撤銷”時,從堆疊 A 中彈出最近的操作,並將其壓入堆疊 B
    3. 當用戶執行“反撤銷”時,從堆疊 B 中彈出最近的操作,並將其壓入堆疊 A
    ","path":["第 5 章   堆疊與佇列","5.4   小結"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章   樹","text":"

    Abstract

    參天大樹充滿生命力,根深葉茂,分枝扶疏。

    它為我們展現了資料分治的生動形態。

    ","path":["第 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 章   樹"],"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 ,因此我們無法僅憑該序列來推測 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// 使用 int 的包裝類別 Integer ,就可以使用 null 來標記空位\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
    /* 二元樹的陣列表示 */\n// 使用 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// 使用 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// 使用 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 年 G. M. Adelson-Velsky 和 E. M. 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 樹既是二元搜尋樹,也是平衡二元樹,同時滿足這兩類二元樹的所有性質,因此是一種平衡二元搜尋樹(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    # 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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  // 以 child 為原點,將 node 向右旋轉\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            // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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    // 以 child 為原點,將 node 向右旋轉\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  # 以 child 為原點,將 node 向右旋轉\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_1","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    # 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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  // 以 child 為原點,將 node 向左旋轉\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            // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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    // 以 child 為原點,將 node 向左旋轉\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  # 以 child 為原點,將 node 向左旋轉\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":"

    對於圖 7-30 中的失衡節點 3 ,僅使用左旋或右旋都無法使子樹恢復平衡。此時需要先對 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-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_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_2","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_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.valnum 之間的大小關係。

    • 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 or 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        # 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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    // 用 tmp 覆蓋 cur\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 or 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            // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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        // 用 tmp 覆蓋 cur\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 or 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    # 用 tmp 覆蓋 cur\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
    視覺化執行

    全螢幕觀看 >

    ","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# 在 n1 -> n2 中間插入節點 P\nn1.left = p\np.left = n2\n# 刪除節點 P\nn1.left = n2\n
    binary_tree.cpp
    /* 插入與刪除節點 */\nTreeNode* P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1->left = P;\nP->left = n2;\n// 刪除節點 P\nn1->left = n2;\n// 釋放記憶體\ndelete P;\n
    binary_tree.java
    TreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.cs
    /* 插入與刪除節點 */\nTreeNode P = new(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.go
    /* 插入與刪除節點 */\n// 在 n1 -> n2 中間插入節點 P\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// 刪除節點 P\nn1.Left = n2\n
    binary_tree.swift
    let P = TreeNode(x: 0)\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P\nP.left = n2\n// 刪除節點 P\nn1.left = n2\n
    binary_tree.js
    /* 插入與刪除節點 */\nlet P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.ts
    /* 插入與刪除節點 */\nconst P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.dart
    /* 插入與刪除節點 */\nTreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P;\nP.left = n2;\n// 刪除節點 P\nn1.left = n2;\n
    binary_tree.rs
    let p = TreeNode::new(0);\n// 在 n1 -> n2 中間插入節點 P\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// 在 n1 -> n2 中間插入節點 P\nn1->left = P;\nP->left = n2;\n// 刪除節點 P\nn1->left = n2;\n// 釋放記憶體\nfree(P);\n
    binary_tree.kt
    val P = TreeNode(0)\n// 在 n1 -> n2 中間插入節點 P\nn1.left = P\nP.left = n2\n// 刪除節點 P\nn1.left = n2\n
    binary_tree.rb
    # 插入與刪除節點\n_p = TreeNode.new(0)\n# 在 n1 -> n2 中間插入節點 _p\nn1.left = _p\n_p.left = n2\n# 刪除節點\nn1.left = n2\n
    視覺化執行

    全螢幕觀看 >

    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:右旋操作是處理失衡節點 nodechildgrand_child 之間的關係,那 node 的父節點和 node 原來的連線不需要維護嗎?右旋操作後豈不是斷掉了?

    我們需要從遞迴的視角來看這個問題。右旋操作 right_rotate(root) 傳入的是子樹的根節點,最終 return child 返回旋轉之後的子樹的根節點。子樹的根節點和其父節點的連線是在該函式返回後完成的,不屬於右旋操作的維護範圍。

    Q:在 C++ 中,函式被劃分到 privatepublic 中,這方面有什麼考量嗎?為什麼要將 height() 函式和 updateHeight() 函式分別放在 publicprivate 中呢?

    主要看方法的使用範圍,如果方法只在類別內部使用,那麼就設計為 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/zh-hant/stylesheets/animation_player.css b/zh-hant/stylesheets/animation_player.css index fdd5531f7..7ff6baf86 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: 20260410024928 */ +/*! update cache: 20260410223931 */ diff --git a/zh-hant/stylesheets/extra.css b/zh-hant/stylesheets/extra.css index 09e1ed99c..3779bede0 100644 --- a/zh-hant/stylesheets/extra.css +++ b/zh-hant/stylesheets/extra.css @@ -790,4 +790,4 @@ a:hover .device-on-hover { flex: 1 1 30%; } } -/*! update cache: 20260410024928 */ +/*! update cache: 20260410223931 */ diff --git a/zh-hant/stylesheets/giscus-dark.css b/zh-hant/stylesheets/giscus-dark.css index 5b30f3b2a..e3acf42ea 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: 20260410024928 */ +/*! update cache: 20260410223931 */ diff --git a/zh-hant/stylesheets/giscus-light.css b/zh-hant/stylesheets/giscus-light.css index 6ac9af14d..69638cc94 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: 20260410024928 */ +/*! update cache: 20260410223931 */