Ենթադրենք, մեզ տրվել է այս խնդիրը.
Հաշվի առնելով ամբողջական երկուական ծառի
root
-ը, վերադարձրեք ծառի հանգույցների թիվը:
Ըստ Վիքիպեդիայի-ի՝ յուրաքանչյուր մակարդակ, բացառությամբ, հնարավոր է, վերջինի, ամբողջությամբ լցված է ամբողջական երկուական ծառի մեջ, և վերջին մակարդակի բոլոր հանգույցները հնարավորինս հեռու են մնացել: Այն կարող է ունենալ
1
-ից2h
հանգույցներ՝ ներառյալ վերջինh
մակարդակը:
Նախագծեք ալգորիթմ, որն աշխատում է
O(n)
-ից պակաս ժամանակով:
Ամբողջական երկուական ծառ
Նախ, եկեք սահմանենք, թե ինչ է ամբողջականերկուական ծառը: Ամբողջական Երկուական ծառը ծառի կառուցվածք է, որն ունի nխորություն, և յուրաքանչյուր հանգույց մինչև n-1 խորությունը լցված է: nխորության վրա ծառի կառուցվածքը լցված է ձախից և լցված է դեպի աջ:
Վերևի նկարում կարող եք տեսնել, որ ներքևի մակարդակը, n խորության վրա մասամբ լցված է դեպի ձախ: Քանի դեռ հանգույցները լցված են ներքևում՝ ձախից -› աջից, մենք կարող ենք ասել, որ երկուական ծառը ամբողջական է:
Ինչպե՞ս ենք սա լուծում:
Խնդրում այն մեզ հարցնում է, թե արդյոք մենք կարող ենք դա լուծել O(n)
-ից քիչ ժամանակում: Եթե մենք օգտագործենք որևէ մեթոդ, ինչպիսին է լայնության առաջին որոնումը կամ խորությունը առաջին որոնումը, վստահաբար, մենք կարող ենք դա հեշտությամբ լուծել, բայց դա կլինի O(n)
ժամանակում:
Մեզ մնում է միայն անցնել յուրաքանչյուր հանգույցի վրայով և ավելացնել հաշվիչը, բայց դա այն չէ, ինչ հարցն է պահանջում: Մենք պետք է լուծենք սա ավելի արագ, քան O(n)
:
Սա ավելի օպտիմալ կերպով լուծելու համար մենք կարող ենք այս խնդիրը բաժանել երկու մասի.
- Գտեք հանգույցների քանակը մինչև վերջին մակարդակը (վերին մակարդակները):
- Գտնելով հանգույցների թիվը վերջին մակարդակում (ներքևի մակարդակ):
Հանգույցների քանակը վերին մակարդակներում
Մինչևներքևիմակարդակը մենք գիտենք, որ երկուական ծառը լի է և ամբողջական: Միակ անհայտը ներքևի մակարդակն է: Որ հետո կլուծենք։ Առայժմ, եկեք գտնենք հանգույցների թիվը յուրաքանչյուր մակարդակում, բացի վերջինից:
Ահա թե ինչ տեսք կունենա մեր օրինակում.
Ուշադրություն դարձրեք, որ այստեղ կա 3 մակարդակ: Հանգույցների ընդհանուր թիվը 7 է:Մենք կարող ենք դա լուծել պարզ մաթեմատիկական հավասարմամբ.
Մենք տեսնում ենք, որ 2³ − 1 = 7,որը հավասար է վերին մակարդակներում գտնվող հանգույցների քանակին: Սա ճիշտ կլինի ամեն անգամ:
Խորություն
Նախքան վերին մակարդակներում հանգույցների քանակը որոշելը, նախ պետք է որոշենք վերին մակարդակների քանակը:
Սա պարզապես կլինի ընդհանուր մակարդակների թիվը (խորությունը) -1: Այսպիսով, մենք պետք է գտնենք խորությունը:
Քանի որ մենք ունենք ամբողջական երկուական ծառ, մենք գիտենք, որ ծառի առավելագույն խորությունը միշտ կլինի ամենահեռավոր ձախ կողմում, բնականաբար: Այսպիսով, մեզ անհրաժեշտ է միայն անցնել ծառի ձախ կողմը և ավելացնել հաշվիչը, որը մենք կսկսենք 0-ից:
public int findDepth(TreeNode cur, int depth) { if(cur == null) return depth; depth++; return findDepth(cur.left, depth); }
Այն ռեկուրսիվորեն կանվանի իրեն՝ ավելացնելով depth
, անցնելով ձախ կողմում, մինչև հասնի null արժեքի: Այդ պահին այն վերադարձնում է depth
-ը վերևում:
Եկեք սկսենք գրել մեր countNodes()
մեթոդի կոդը.
public int countNodes(TreeNode root) { int depth = findDepth(root, 0); if(depth == 0 || depth == 1) return depth; int numOfUpperNodes = (int) (Math.pow(2, depth-1)-1); ... } public int findDepth(TreeNode cur, int depth) { if(cur == null) return depth; depth++; return findDepth(cur.left, depth); }
Այստեղ մենք օգտագործում ենք findDepth()
մեթոդը՝ ծառի ընդհանուր խորությունը ստանալու համար:
Մենք նախ հոգում ենք մի քանի անկյունային դեպքերի մասին, որոնք լավագույնս կարգավորվելու են սկզբում: Եթե արմատը null է կամ չունի ձախարժեք, ապա մենք պարզապես վերադարձնում ենք գտնված խորությունը:
Հակառակ դեպքում, մենք գտնում ենք վերին տողերը, որոնք մենք անվանում ենք numOfUpperNodes
և սահմանում ենք այն 2ⁿ − 1 բանաձևը, որը մենք հաստատել ենք ավելի վաղ:
Այժմ մենք պետք է գտնենք ներքևի մակարդակում հանգույցների քանակը:
Հանգույցների թիվը ներքևիմակարդակում
Եթե մենք նայենք ներքևի մակարդակին, ապա կարող ենք տեսնել, որ մենք կարող ենք դիտել այն որպես հանգույցների ցանկ, ինչպես ցանկացած այլ զանգված, բայց ինչպես մենք մուտք ենք գործում այս արժեքները կտարբերվեն:
Այն ամենը, ինչ մենք փորձում ենք անել, այս զանգվածում գոյություն ունեցող վերջին հանգույցն է: Մեր օրինակում 1–4 ինդեքսները գոյություն ունեն, իսկ 5–7 ոչ: Այս դեպքում, ինդեքս 4 ստորին մակարդակի վերջին հանգույցն է:
Սա օպտիմալ կերպով որոնելու համար մենք կարող ենք իրականացնել երկուական որոնում: Բայց բարդ մասը դա ծառի վրա իրականացնելն է: Ինչպե՞ս ենք մենք անցնում յուրաքանչյուր ցուցանիշի:
Եկեք նախ անդրադառնանք, թե ինչպես պետք է իրականացնենք երկուական որոնումը մեր օրինակում:
public int countNodes(TreeNode root) { int depth = findDepth(root, 0); if(depth == 0 || depth == 1) return depth; int numOfUpperNodes = (int) (Math.pow(2, depth-1)-1); int left = 0; int right = numOfUpperNodes; // On the level below there is numberOfUpperNodes + 1 (index is -1) // Perform binary search while(left < right) { int idxToFind = (int) Math.ceil((double)(left + right) / 2); boolean nodeExists = nodeExists(idxToFind, depth, root, numOfUpperNodes); // Check if node exists during at the index to find if(nodeExists) { left = idxToFind; } else { right = idxToFind-1; } } return numOfUpperNodes + left + 1; }
Այստեղ մենք օգտագործում ենք երկուական որոնման պարզ իրականացում left
և right
ցուցիչներով: Մենք կանցնենք nodeExists()
ֆունկցիային ավելի ուշ, բայց առայժմ ենթադրենք.
nodeExists
-ը կսահմանվի true, եթե հանգույցը գոյություն ունիidxToFind
-ում:nodeExists
-ը կսահմանվի false, եթե հանգույցը չկաidxToFind
-ում:
Սկզբում մենք սկսում ենք left
-ով ինդեքս 0-ով և right
ով՝ ինդեքս 7-ով: Մենք վերցնում ենք միջնակետը՝ idxToFind
ը՝ որպես ինդեքս 4:
Երբ զանգում ենք nodeExists()
-ին, տեսնում ենք, որ ինդեքսի 4-ում կա հանգույցը գոյություն ունի:
Մենք left
-ը տեղափոխում ենք այնտեղ, որտեղ idxToFind
էր, որը 4-րդ ինդեքսն է: Մենք վերցնում ենք միջնակետը և ստանում ենք նորidxToFind
, որը գտնվում է ինդեքս 6-ում:
Մենք տեսնում ենք, որ հանգույց գոյություն չունի այս ինդեքս 6-ում:
Մենք right
ցուցիչը տեղափոխում ենք ձախ դեպի idxToFind-1
, որը ինդեքս 5 է: Մենք ստանում ենք նոր idxToFind
ինդեքս 5-ում:
Մենք տեսնում ենք, որ հանգույց գոյություն չունի այս ինդեքս 5-ում:
Մենք right
ցուցիչը տեղափոխում ենք ձախ դեպի idxToFind-1
, որը ինդեքս 4 է:
Այժմ, left
== right
, որը խախտում է օղակը:
Կարող ենք ասել, որ left
ինդեքսը ներքևի մակարդակի վերջին ինդեքսն է, որն ունի հանգույց:
Այսպիսով, քանի որ ինդեքս = 4, կարող ենք ասել, որ ներքևի մակարդակում կան 5 հանգույցներ.
- 0, 1, 2, 3, 4
Այժմ, մենք պետք է պարզենք, թե ինչպես ենքորոշում, արդյոք հանգույցը գոյություն ունի ներքևի մակարդակի որոշակի ինդեքսում:
Արդյո՞ք հանգույցը գոյություն ունի:
Այժմ մենք գիտենք, թե ինչպես օգտագործել երկուական որոնումը ներքևի մակարդակում՝ գտնելու վերջին ինդեքսը, որտեղ գոյություն ունի հանգույց: Բայց քանի որ տվյալները պահվում են ծառի կառուցվածքի ստորին մակարդակում, մենք պետք է որոշենք, թե ինչպես կարելի է հասնելյուրաքանչյուր ինդեքսին:
Եկեք նայենք կոդը, որը մենք օգտագործում ենք՝ որոշելու համար, թե արդյո՞ք հանգույցը գոյություն ունի ներքևի մակարդակի ինդեքսում:
public boolean nodeExists( int idxToFind, int depth, TreeNode node, int numOfUpperNodes) { int left = 0; int right = numOfUpperNodes; int count = 1; while(count < depth) { int mid = (int) Math.ceil((double)(left + right) / 2); if(idxToFind >= mid) { node = node.right; left = mid; } else { node = node.left; right = mid - 1; } count++; } return node != null; }
Վերևի կոդում մուտքերն են.
idxToFind
(ինդեքս գտնելու համար)depth
node
(արմատը)numOfUpperNodes
(վերին հանգույցների քանակը)
Մենք սահմանել ենք left
և right
համապատասխանաբար 0 և numOfUpperNodes
: Մենք կարող ենք պատկերացնել left
-ը և right
-ը որպես ստորին շերտի սահմաններ: Մեր դեպքում սա 0և 7 է:
Մենք գործարկում ենք հանգույց, մինչդեռ count
‹ depth
և ավելացնում ենք count
օղակի յուրաքանչյուր կրկնման ժամանակ: Սա այնպես է, որ մենք ավելի չգնանք ստորին մակարդակից:
Ներքևի մակարդակում, երբ մենք կանգ առնենք, մեզ կամ կմնա node
, կամ node
-ը ցույց կտա null:
Ներքևում գտնվող ճիշտ ինդեքսի մեր ճանապարհը գտնելու համար.
Յուրաքանչյուր կրկնության մեջ մենք սահմանում ենք mid
կետ:
Եթե idxToFind
-ը մեծ է կամ հավասար է միջինին, մենք գնում ենք աջ:
- Մենք սահմանել ենք
node
=node.right
- Մենք տեղափոխում ենք
left
__________
Եթե idxToFind
-ը միջինից փոքր է, մենք գնում ենք ձախ:
- Մենք սահմանել ենք
node
=node.left
- Մենք տեղափոխում ենք
right
mid — 1
Նայեք այս անիմացիային.
Մտածեք դրա մասին այսպես.
Count
սկսվում է 1-ին:
Սկզբում մենք կարող ենք արմատից շարժվել ցանկացած ուղղությամբ: Սահմաններն են0–7։ Եթե mid
-ը 4 է, ապա մենք պետք է գնանք ձախ, քանի որ մեր հանգույցը գտնվում է ինդեքս 3-ում և չի կարող գոյություն ունենալ mid
.Count
ավելացումներից հետո: դեպի 2.
Այն բանից հետո, երբ մենք ընտրում ենք գնալ ձախ, այժմ սահմաններն են0–3: Ահա թե ինչու մենք right
-ը սահմանեցինք mid-1
: mid
-ը 2 է, իսկ մեր ցուցանիշը 3 է, ուստի մենք պետք է գնանք ճիշտ: Count
-ն այժմ 3 է:
Երբ մենք գնում ենք աջ, սահմանները նորից փոքրանում են մինչև 2–3: Վերջապես, մեր mid
-ը 3 է, ուստի մենք նորից գնում ենք աջ: Count
այժմ 4 է:
Քանի որ count
-ն այժմ հավասար է 4-ի, այն խախտում է while օղակը, և քանի որ node
-ը, որի վրա մենք հայտնվեցինք, ոչ զրոյական էր, մենք կարող ենք վերադարձնել true:
Եթե, օրինակ, մեր ինդեքսը լիներ5, ապա կրկնությունները նման կլինեն.
- Ճիշտ
- Ձախ
- Ճիշտ
Վերջին կրկնության ժամանակ, երբ աջ տեղափոխվենք 6 հանգույցից, node
-ը ցույց կտա null: Սա կդարձնի ֆունկցիան վերադառնալ false:
Կոդ
Հիմա, երբ մենք ամեն ինչ հավաքում ենք, մենք ունենք մեր լուծումը։
public int countNodes(TreeNode root) { int depth = findDepth(root, 0); if(depth == 0 || depth == 1) return depth; int numOfUpperNodes = (int) (Math.pow(2, depth-1)-1); int left = 0; int right = numOfUpperNodes; // On the level below there is numberOfUpperNodes + 1 (index is -1) // Perform binary search while(left < right) { int idxToFind = (int) Math.ceil((double)(left + right) / 2); boolean nodeExists = nodeExists(idxToFind, depth, root, numOfUpperNodes); // Check if node exists during at the index to find if(nodeExists) { left = idxToFind; } else { right = idxToFind-1; } } return numOfUpperNodes + left + 1; } public boolean nodeExists( int idxToFind, int depth, TreeNode node, int numOfUpperNodes) { int left = 0; int right = numOfUpperNodes; int count = 1; while(count < depth) { int mid = (int) Math.ceil((double)(left + right) / 2); if(idxToFind >= mid) { node = node.right; left = mid; } else { node = node.left; right = mid - 1; } count++; } return node != null; } public int findDepth(TreeNode cur, int depth) { if(cur == null) return depth; depth++; return findDepth(cur.left, depth); }
Կատարման վերլուծություն
Ժամանակի բարդություն.
- Ձեր
countNodes
ֆունկցիայի ժամանակային բարդության գերիշխող գործոնը երկուական որոնման օղակն է: Քանի որ դուք կատարում եք երկուական որոնում հավասարակշռված երկուական ծառի վրա, երկուական որոնման ժամանակային բարդությունը O(log n) է, որտեղ «n»-ը երկուական ծառի հանգույցների թիվն է: findDepth
ֆունկցիան, որը հաշվարկում է ծառի խորությունը, ունի O(log n) ժամանակային բարդություն հավասարակշռված երկուական ծառում, քանի որ այն անցնում է ձախ ենթածառով:nodeExists
ֆունկցիան նաև կատարում է երկուական որոնում յուրաքանչյուր մակարդակում, և քանի որ երկուական որոնման խորությունը լոգարիթմական է, դրա ժամանակային բարդությունը O(log n է):- Ընդհանուր առմամբ,
countNodes
ֆունկցիայի ժամանակային բարդության մեջ գերակշռում է երկուական որոնումը, ուստի այն O(log n) է, որտեղ «n»-ը երկուական ծառի հանգույցների թիվն է:
Տիեզերական բարդություն.
- Ձեր
countNodes
ֆունկցիայի տարածության բարդությունը որոշվում էfindDepth
ֆունկցիայի ևnodeExists
ֆունկցիայի ռեկուրսիոն կույտերի համար պահանջվող տարածությամբ: Քանի որ այս ֆունկցիաները կոչվում են ռեկուրսիվորեն ծառի յուրաքանչյուր մակարդակի համար, ռեկուրսիայի առավելագույն խորությունը երկուական ծառի բարձրությունն է։ - Հավասարակշռված երկուական ծառում բարձրությունը log₂(n) է, որտեղ «n»-ը հանգույցների թիվն է: Այսպիսով, ռեկուրսիոն կույտերի համար պահանջվող առավելագույն տարածությունը O(log n) է:
- Բացի այդ, դուք օգտագործում եք մշտական քանակությամբ լրացուցիչ տարածություն այնպիսի փոփոխականների համար, ինչպիսիք են
depth
,numOfUpperNodes
,left
,right
,idxToFind
ևnodeExists
բուլյան գործառույթը: Այս փոփոխականները կախված չեն մուտքագրման չափից և մշտական տարածություն են: - Հետևաբար,
countNodes
ֆունկցիայի ընդհանուր տարածության բարդությունը O(log n) է` պայմանավորված ռեկուրսիոն կույտի տարածությամբ և փոփոխականների կողմից օգտագործվող հաստատուն տարածությամբ:
Մենք գտել ենք լուծում, որն ավելի լավ է գործում, քանO(n): Հուսով եմ, որ սա օգտակար էր: