Ենթադրենք, մեզ տրվել է այս խնդիրը.

Հաշվի առնելով ամբողջական երկուական ծառի 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 է:

Մենք գործարկում ենք հանգույց, մինչդեռ countdepth և ավելացնում ենք count օղակի յուրաքանչյուր կրկնման ժամանակ: Սա այնպես է, որ մենք ավելի չգնանք ստորին մակարդակից:

Ներքևի մակարդակում, երբ մենք կանգ առնենք, մեզ կամ կմնա node, կամ node-ը ցույց կտա null:

Ներքևում գտնվող ճիշտ ինդեքսի մեր ճանապարհը գտնելու համար.

Յուրաքանչյուր կրկնության մեջ մենք սահմանում ենք mid կետ:

Եթե ​​idxToFind-ը մեծ է կամ հավասար է միջինին, մենք գնում ենք աջ:

  • Մենք սահմանել ենք node = node.right
  • Մենք տեղափոխում ենք left__________

Եթե ​​idxToFind-ը միջինից փոքր է, մենք գնում ենք ձախ:

  • Մենք սահմանել ենք node = node.left
  • Մենք տեղափոխում ենք right mid — 1

Նայեք այս անիմացիային.

Մտածեք դրա մասին այսպես.

Count սկսվում է 1-ին:

Սկզբում մենք կարող ենք արմատից շարժվել ցանկացած ուղղությամբ: Սահմաններն են0–7։ Եթե ​​mid4 է, ապա մենք պետք է գնանք ձախ, քանի որ մեր հանգույցը գտնվում է ինդեքս 3-ում և չի կարող գոյություն ունենալ mid.Count ավելացումներից հետո: դեպի 2.

Այն բանից հետո, երբ մենք ընտրում ենք գնալ ձախ, այժմ սահմաններն են0–3: Ահա թե ինչու մենք right-ը սահմանեցինք mid-1: mid2 է, իսկ մեր ցուցանիշը 3 է, ուստի մենք պետք է գնանք ճիշտ: Count-ն այժմ 3 է:

Երբ մենք գնում ենք աջ, սահմանները նորից փոքրանում են մինչև 2–3: Վերջապես, մեր mid3 է, ուստի մենք նորից գնում ենք աջ: 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): Հուսով եմ, որ սա օգտակար էր:

LeetCode արդյունք